diff --git a/internal/features/anime/errors.go b/api/anime/errors.go similarity index 100% rename from internal/features/anime/errors.go rename to api/anime/errors.go diff --git a/internal/features/anime/handler.go b/api/anime/handler.go similarity index 99% rename from internal/features/anime/handler.go rename to api/anime/handler.go index 43240ef..71bc493 100644 --- a/internal/features/anime/handler.go +++ b/api/anime/handler.go @@ -9,10 +9,10 @@ import ( "strconv" "strings" - "mal/internal/database" - "mal/internal/jikan" - "mal/internal/shared/middleware" - "mal/internal/templates" + "mal/internal/db" + "mal/integrations/jikan" + "mal/internal/middleware" + "mal/web/templates" ) func deduplicateAnimes(animes []jikan.Anime) []jikan.Anime { diff --git a/internal/features/auth/auth.go b/api/auth/auth.go similarity index 99% rename from internal/features/auth/auth.go rename to api/auth/auth.go index 87729b0..836a976 100644 --- a/internal/features/auth/auth.go +++ b/api/auth/auth.go @@ -13,7 +13,7 @@ import ( "golang.org/x/crypto/bcrypt" - "mal/internal/database" + "mal/internal/db" ) var ( diff --git a/internal/features/auth/handler.go b/api/auth/handler.go similarity index 98% rename from internal/features/auth/handler.go rename to api/auth/handler.go index 36d8642..ddc0d04 100644 --- a/internal/features/auth/handler.go +++ b/api/auth/handler.go @@ -3,7 +3,7 @@ package auth import ( "net/http" - "mal/internal/templates" + "mal/web/templates" ) type Handler struct { diff --git a/internal/features/playback/allanime_client.go b/api/playback/allanime_client.go similarity index 100% rename from internal/features/playback/allanime_client.go rename to api/playback/allanime_client.go diff --git a/internal/features/playback/handler.go b/api/playback/handler.go similarity index 98% rename from internal/features/playback/handler.go rename to api/playback/handler.go index 9ed2280..b0af077 100644 --- a/internal/features/playback/handler.go +++ b/api/playback/handler.go @@ -14,10 +14,10 @@ import ( "strings" "time" - "mal/internal/database" - "mal/internal/jikan" - "mal/internal/shared/middleware" - "mal/internal/templates" + "mal/internal/db" + "mal/integrations/jikan" + "mal/internal/middleware" + "mal/web/templates" ) type Handler struct { diff --git a/internal/features/playback/http_utils.go b/api/playback/http_utils.go similarity index 100% rename from internal/features/playback/http_utils.go rename to api/playback/http_utils.go diff --git a/internal/features/playback/progress.go b/api/playback/progress.go similarity index 99% rename from internal/features/playback/progress.go rename to api/playback/progress.go index 2babb15..03e6e7d 100644 --- a/internal/features/playback/progress.go +++ b/api/playback/progress.go @@ -9,7 +9,7 @@ import ( "github.com/google/uuid" - "mal/internal/database" + "mal/internal/db" ) func (s *Service) SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64, animeSeed *database.UpsertAnimeParams) error { diff --git a/internal/features/playback/provider_extractor.go b/api/playback/provider_extractor.go similarity index 100% rename from internal/features/playback/provider_extractor.go rename to api/playback/provider_extractor.go diff --git a/internal/features/playback/proxy_security.go b/api/playback/proxy_security.go similarity index 100% rename from internal/features/playback/proxy_security.go rename to api/playback/proxy_security.go diff --git a/internal/features/playback/proxy_security_test.go b/api/playback/proxy_security_test.go similarity index 97% rename from internal/features/playback/proxy_security_test.go rename to api/playback/proxy_security_test.go index 9c09ea7..2253186 100644 --- a/internal/features/playback/proxy_security_test.go +++ b/api/playback/proxy_security_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "mal/internal/database" + "mal/internal/db" ) func TestNormalizeProxyURLRejectsLocalhost(t *testing.T) { diff --git a/internal/features/playback/service_base.go b/api/playback/service_base.go similarity index 99% rename from internal/features/playback/service_base.go rename to api/playback/service_base.go index 474c617..f070fc8 100644 --- a/internal/features/playback/service_base.go +++ b/api/playback/service_base.go @@ -5,7 +5,7 @@ import ( "database/sql" "errors" "fmt" - "mal/internal/database" + "mal/internal/db" "net/http" "strconv" "strings" diff --git a/internal/features/playback/service_http.go b/api/playback/service_http.go similarity index 100% rename from internal/features/playback/service_http.go rename to api/playback/service_http.go diff --git a/internal/features/playback/service_proxy.go b/api/playback/service_proxy.go similarity index 100% rename from internal/features/playback/service_proxy.go rename to api/playback/service_proxy.go diff --git a/internal/features/playback/service_ranking.go b/api/playback/service_ranking.go similarity index 100% rename from internal/features/playback/service_ranking.go rename to api/playback/service_ranking.go diff --git a/internal/features/playback/service_resolution.go b/api/playback/service_resolution.go similarity index 100% rename from internal/features/playback/service_resolution.go rename to api/playback/service_resolution.go diff --git a/internal/features/playback/service_sources.go b/api/playback/service_sources.go similarity index 100% rename from internal/features/playback/service_sources.go rename to api/playback/service_sources.go diff --git a/internal/features/playback/service_utils.go b/api/playback/service_utils.go similarity index 100% rename from internal/features/playback/service_utils.go rename to api/playback/service_utils.go diff --git a/internal/features/playback/types.go b/api/playback/types.go similarity index 100% rename from internal/features/playback/types.go rename to api/playback/types.go diff --git a/internal/features/watchlist/handler.go b/api/watchlist/handler.go similarity index 99% rename from internal/features/watchlist/handler.go rename to api/watchlist/handler.go index 4cf8d29..94d68ce 100644 --- a/internal/features/watchlist/handler.go +++ b/api/watchlist/handler.go @@ -8,9 +8,9 @@ import ( "slices" "strconv" - "mal/internal/database" - "mal/internal/shared/middleware" - "mal/internal/templates" + "mal/internal/db" + "mal/internal/middleware" + "mal/web/templates" ) type Handler struct { diff --git a/internal/features/watchlist/service.go b/api/watchlist/service.go similarity index 99% rename from internal/features/watchlist/service.go rename to api/watchlist/service.go index 855c36e..d5bb9d2 100644 --- a/internal/features/watchlist/service.go +++ b/api/watchlist/service.go @@ -10,7 +10,7 @@ import ( "github.com/google/uuid" - "mal/internal/database" + "mal/internal/db" ) type Service struct { diff --git a/internal/features/watchlist/service_test.go b/api/watchlist/service_test.go similarity index 99% rename from internal/features/watchlist/service_test.go rename to api/watchlist/service_test.go index 5a69be5..f60193d 100644 --- a/internal/features/watchlist/service_test.go +++ b/api/watchlist/service_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "mal/internal/database" + "mal/internal/db" ) type fakeQuerier struct { diff --git a/cmd/server/main.go b/cmd/server/main.go index 9f8ba43..922786b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -14,11 +14,11 @@ import ( _ "github.com/mattn/go-sqlite3" - "mal/internal/database" - "mal/internal/features/auth" - "mal/internal/jikan" + dbpkg "mal/internal/db" + "mal/api/auth" + "mal/integrations/jikan" "mal/internal/server" - "mal/internal/shared/middleware" + "mal/pkg/middleware" "mal/internal/worker" ) @@ -30,11 +30,11 @@ func main() { defer db.Close() migrationsDir := migrationsDir() - if err := database.RunMigrations(db, migrationsDir); err != nil { + if err := dbpkg.RunMigrations(db, migrationsDir); err != nil { log.Fatalf("failed to run migrations: %v", err) } - queries := database.New(db) + queries := dbpkg.New(db) jikanClient := jikan.NewClient(queries) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) diff --git a/internal/jikan/anime.go b/integrations/jikan/anime.go similarity index 100% rename from internal/jikan/anime.go rename to integrations/jikan/anime.go diff --git a/internal/jikan/client.go b/integrations/jikan/client.go similarity index 99% rename from internal/jikan/client.go rename to integrations/jikan/client.go index a2fc07a..f675e15 100644 --- a/internal/jikan/client.go +++ b/integrations/jikan/client.go @@ -13,7 +13,7 @@ import ( "sync" "time" - "mal/internal/database" + "mal/internal/db" ) type Client struct { diff --git a/internal/jikan/constants.go b/integrations/jikan/constants.go similarity index 100% rename from internal/jikan/constants.go rename to integrations/jikan/constants.go diff --git a/internal/jikan/episodes.go b/integrations/jikan/episodes.go similarity index 100% rename from internal/jikan/episodes.go rename to integrations/jikan/episodes.go diff --git a/internal/jikan/recommendations.go b/integrations/jikan/recommendations.go similarity index 100% rename from internal/jikan/recommendations.go rename to integrations/jikan/recommendations.go diff --git a/internal/jikan/relations.go b/integrations/jikan/relations.go similarity index 99% rename from internal/jikan/relations.go rename to integrations/jikan/relations.go index 35135d0..7575e3d 100644 --- a/internal/jikan/relations.go +++ b/integrations/jikan/relations.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "mal/internal/watchorder" + "mal/integrations/watchorder" ) const chiakiWatchOrderURL = "https://chiaki.site/?/tools/watch_order/id/%d" diff --git a/internal/jikan/relations_test.go b/integrations/jikan/relations_test.go similarity index 100% rename from internal/jikan/relations_test.go rename to integrations/jikan/relations_test.go diff --git a/internal/jikan/search.go b/integrations/jikan/search.go similarity index 100% rename from internal/jikan/search.go rename to integrations/jikan/search.go diff --git a/internal/jikan/seasons.go b/integrations/jikan/seasons.go similarity index 100% rename from internal/jikan/seasons.go rename to integrations/jikan/seasons.go diff --git a/internal/jikan/studio.go b/integrations/jikan/studio.go similarity index 100% rename from internal/jikan/studio.go rename to integrations/jikan/studio.go diff --git a/internal/jikan/studio_test.go b/integrations/jikan/studio_test.go similarity index 98% rename from internal/jikan/studio_test.go rename to integrations/jikan/studio_test.go index 34da87c..b3b53dc 100644 --- a/internal/jikan/studio_test.go +++ b/integrations/jikan/studio_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - "mal/internal/database" + "mal/internal/db" ) type staleCacheQuerier struct { diff --git a/internal/jikan/types.go b/integrations/jikan/types.go similarity index 100% rename from internal/jikan/types.go rename to integrations/jikan/types.go diff --git a/internal/watchorder/watch_order.go b/integrations/watchorder/watch_order.go similarity index 100% rename from internal/watchorder/watch_order.go rename to integrations/watchorder/watch_order.go diff --git a/internal/watchorder/watch_order_test.go b/integrations/watchorder/watch_order_test.go similarity index 100% rename from internal/watchorder/watch_order_test.go rename to integrations/watchorder/watch_order_test.go diff --git a/internal/database/db.go b/internal/db/db.go similarity index 100% rename from internal/database/db.go rename to internal/db/db.go diff --git a/internal/database/helpers.go b/internal/db/helpers.go similarity index 100% rename from internal/database/helpers.go rename to internal/db/helpers.go diff --git a/internal/database/migrate.go b/internal/db/migrate.go similarity index 100% rename from internal/database/migrate.go rename to internal/db/migrate.go diff --git a/internal/database/models.go b/internal/db/models.go similarity index 100% rename from internal/database/models.go rename to internal/db/models.go diff --git a/internal/database/querier.go b/internal/db/querier.go similarity index 100% rename from internal/database/querier.go rename to internal/db/querier.go diff --git a/internal/database/queries.sql b/internal/db/queries.sql similarity index 100% rename from internal/database/queries.sql rename to internal/db/queries.sql diff --git a/internal/database/queries.sql.go b/internal/db/queries.sql.go similarity index 100% rename from internal/database/queries.sql.go rename to internal/db/queries.sql.go diff --git a/internal/shared/middleware/access.go b/internal/middleware/access.go similarity index 98% rename from internal/shared/middleware/access.go rename to internal/middleware/access.go index f088bd7..03e900b 100644 --- a/internal/shared/middleware/access.go +++ b/internal/middleware/access.go @@ -4,7 +4,7 @@ import ( "net/http" "strings" - "mal/internal/database" + "mal/internal/db" ) type AccessPolicy struct { diff --git a/internal/shared/middleware/auth.go b/internal/middleware/auth.go similarity index 96% rename from internal/shared/middleware/auth.go rename to internal/middleware/auth.go index 74c08c6..850c73a 100644 --- a/internal/shared/middleware/auth.go +++ b/internal/middleware/auth.go @@ -5,8 +5,8 @@ import ( "net/http" "strings" - "mal/internal/database" - "mal/internal/features/auth" + "mal/internal/db" + "mal/api/auth" ) type contextKey string diff --git a/internal/server/routes.go b/internal/server/routes.go index 5e61b5b..f5ae046 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -4,13 +4,14 @@ import ( "database/sql" "net/http" - "mal/internal/database" - "mal/internal/features/anime" - "mal/internal/features/auth" - "mal/internal/features/playback" - "mal/internal/features/watchlist" - "mal/internal/jikan" - "mal/internal/shared/middleware" + "mal/internal/db" + "mal/api/anime" + "mal/api/auth" + "mal/api/playback" + "mal/api/watchlist" + "mal/integrations/jikan" + "mal/internal/middleware" + pkgmiddleware "mal/pkg/middleware" ) type Config struct { @@ -70,7 +71,7 @@ func NewRouter(cfg Config) http.Handler { if r.Method == http.MethodGet { authHandler.HandleLoginPage(w, r) } else { - middleware.RateLimitAuth(middleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleLogin))).ServeHTTP(w, r) + pkgmiddleware.RateLimitAuth(pkgmiddleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleLogin))).ServeHTTP(w, r) } }) @@ -84,7 +85,7 @@ func NewRouter(cfg Config) http.Handler { // Wrap mux with global CSRF origin verification and auth checking, // THEN auth context parsing. - protectedHandler := middleware.RequireGlobalAuthWithPolicy(middleware.NewAccessPolicy())(middleware.VerifyOrigin(mux)) + protectedHandler := middleware.RequireGlobalAuthWithPolicy(middleware.NewAccessPolicy())(pkgmiddleware.VerifyOrigin(mux)) authenticatedHandler := middleware.Auth(cfg.AuthService)(protectedHandler) - return middleware.RequestLogger(authenticatedHandler) + return pkgmiddleware.RequestLogger(authenticatedHandler) } diff --git a/internal/shared/middleware/access_test.go b/internal/shared/middleware/access_test.go deleted file mode 100644 index 6954311..0000000 --- a/internal/shared/middleware/access_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package middleware - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "mal/internal/database" -) - -func TestAccessPolicy_IsPublicPath(t *testing.T) { - t.Parallel() - - policy := NewAccessPolicy() - - if !policy.IsPublicPath("/") { - t.Fatal("expected / to be public") - } - - if !policy.IsPublicPath("/api/search") { - t.Fatal("expected /api/search to be public") - } - - if !policy.IsPublicPath("/static/app.css") { - t.Fatal("expected /static/app.css to be public") - } - - if policy.IsPublicPath("/watchlist") { - t.Fatal("expected /watchlist to be private") - } -} - -func TestRequireGlobalAuthWithPolicy_ProtectedPath(t *testing.T) { - t.Parallel() - - policy := AccessPolicy{ - PublicPaths: map[string]struct{}{"/public": {}}, - } - - h := RequireGlobalAuthWithPolicy(policy)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNoContent) - })) - - req := httptest.NewRequest(http.MethodGet, "/private", nil) - rec := httptest.NewRecorder() - - h.ServeHTTP(rec, req) - - if rec.Code != http.StatusFound { - t.Fatalf("expected redirect status, got %d", rec.Code) - } - - if location := rec.Header().Get("Location"); location != "/login" { - t.Fatalf("expected redirect to /login, got %q", location) - } -} - -func TestRequireGlobalAuthWithPolicy_AllowsAuthenticatedUser(t *testing.T) { - t.Parallel() - - policy := AccessPolicy{ - PublicPaths: map[string]struct{}{}, - } - - h := RequireGlobalAuthWithPolicy(policy)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNoContent) - })) - - req := httptest.NewRequest(http.MethodGet, "/private", nil) - ctx := context.WithValue(req.Context(), UserContextKey, &database.User{ID: "user-1"}) - rec := httptest.NewRecorder() - - h.ServeHTTP(rec, req.WithContext(ctx)) - - if rec.Code != http.StatusNoContent { - t.Fatalf("expected status %d, got %d", http.StatusNoContent, rec.Code) - } -} diff --git a/internal/shared/middleware/auth_test.go b/internal/shared/middleware/auth_test.go deleted file mode 100644 index 0874dfb..0000000 --- a/internal/shared/middleware/auth_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package middleware - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "mal/internal/database" -) - -func TestRequireAuth_UnauthenticatedAPIRequest(t *testing.T) { - t.Parallel() - - h := RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - - req := httptest.NewRequest(http.MethodGet, "/api/watchlist", nil) - rec := httptest.NewRecorder() - - h.ServeHTTP(rec, req) - - if rec.Code != http.StatusUnauthorized { - t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code) - } - - if got := rec.Header().Get("HX-Redirect"); got != "/login" { - t.Fatalf("expected HX-Redirect /login, got %q", got) - } -} - -func TestRequireAuth_AuthenticatedRequestPassesThrough(t *testing.T) { - t.Parallel() - - h := RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNoContent) - })) - - req := httptest.NewRequest(http.MethodGet, "/watchlist", nil) - ctx := context.WithValue(req.Context(), UserContextKey, &database.User{ID: "user-1"}) - rec := httptest.NewRecorder() - - h.ServeHTTP(rec, req.WithContext(ctx)) - - if rec.Code != http.StatusNoContent { - t.Fatalf("expected status %d, got %d", http.StatusNoContent, rec.Code) - } -} - -func TestRequireGlobalAuth_AllowsPublicRoute(t *testing.T) { - t.Parallel() - - h := RequireGlobalAuthWithPolicy(NewAccessPolicy())(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - - h.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("expected status %d, got %d", http.StatusOK, rec.Code) - } -} diff --git a/internal/worker/relations.go b/internal/worker/relations.go index a0ee013..30488ad 100644 --- a/internal/worker/relations.go +++ b/internal/worker/relations.go @@ -7,8 +7,8 @@ import ( "log" "time" - "mal/internal/database" - "mal/internal/jikan" + "mal/internal/db" + "mal/integrations/jikan" ) type Worker struct { diff --git a/internal/shared/middleware/csrf.go b/pkg/middleware/csrf.go similarity index 100% rename from internal/shared/middleware/csrf.go rename to pkg/middleware/csrf.go diff --git a/internal/shared/middleware/logging.go b/pkg/middleware/logging.go similarity index 100% rename from internal/shared/middleware/logging.go rename to pkg/middleware/logging.go diff --git a/internal/shared/middleware/ratelimit.go b/pkg/middleware/ratelimit.go similarity index 100% rename from internal/shared/middleware/ratelimit.go rename to pkg/middleware/ratelimit.go diff --git a/internal/shared/ui/anime_card.templ b/web/components/anime_card.templ similarity index 100% rename from internal/shared/ui/anime_card.templ rename to web/components/anime_card.templ diff --git a/internal/shared/ui/anime_list.templ b/web/components/anime_list.templ similarity index 100% rename from internal/shared/ui/anime_list.templ rename to web/components/anime_list.templ diff --git a/internal/shared/ui/empty_state.templ b/web/components/empty_state.templ similarity index 100% rename from internal/shared/ui/empty_state.templ rename to web/components/empty_state.templ diff --git a/internal/shared/ui/icons/icons.templ b/web/components/icons/icons.templ similarity index 100% rename from internal/shared/ui/icons/icons.templ rename to web/components/icons/icons.templ diff --git a/internal/shared/ui/loading.templ b/web/components/loading.templ similarity index 100% rename from internal/shared/ui/loading.templ rename to web/components/loading.templ diff --git a/internal/shared/ui/sort_filter.templ b/web/components/sort_filter.templ similarity index 100% rename from internal/shared/ui/sort_filter.templ rename to web/components/sort_filter.templ diff --git a/internal/templates/anime.templ b/web/templates/anime.templ similarity index 100% rename from internal/templates/anime.templ rename to web/templates/anime.templ diff --git a/internal/templates/auth.templ b/web/templates/auth.templ similarity index 100% rename from internal/templates/auth.templ rename to web/templates/auth.templ diff --git a/internal/templates/catalog.templ b/web/templates/catalog.templ similarity index 100% rename from internal/templates/catalog.templ rename to web/templates/catalog.templ diff --git a/internal/templates/continue_watching.templ b/web/templates/continue_watching.templ similarity index 100% rename from internal/templates/continue_watching.templ rename to web/templates/continue_watching.templ diff --git a/internal/templates/discovery.templ b/web/templates/discovery.templ similarity index 100% rename from internal/templates/discovery.templ rename to web/templates/discovery.templ diff --git a/internal/templates/index.templ b/web/templates/index.templ similarity index 100% rename from internal/templates/index.templ rename to web/templates/index.templ diff --git a/internal/templates/layout.templ b/web/templates/layout.templ similarity index 100% rename from internal/templates/layout.templ rename to web/templates/layout.templ diff --git a/internal/templates/not_found.templ b/web/templates/not_found.templ similarity index 100% rename from internal/templates/not_found.templ rename to web/templates/not_found.templ diff --git a/internal/templates/studio.templ b/web/templates/studio.templ similarity index 100% rename from internal/templates/studio.templ rename to web/templates/studio.templ diff --git a/internal/templates/watch.templ b/web/templates/watch.templ similarity index 100% rename from internal/templates/watch.templ rename to web/templates/watch.templ diff --git a/internal/templates/watchlist.templ b/web/templates/watchlist.templ similarity index 100% rename from internal/templates/watchlist.templ rename to web/templates/watchlist.templ