refactor: reorganize project structure following go standards

This commit is contained in:
2026-04-20 15:54:35 +02:00
parent 055ec1fca9
commit 6df8788749
70 changed files with 43 additions and 187 deletions

View File

@@ -9,10 +9,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"mal/internal/database" "mal/internal/db"
"mal/internal/jikan" "mal/integrations/jikan"
"mal/internal/shared/middleware" "mal/internal/middleware"
"mal/internal/templates" "mal/web/templates"
) )
func deduplicateAnimes(animes []jikan.Anime) []jikan.Anime { func deduplicateAnimes(animes []jikan.Anime) []jikan.Anime {

View File

@@ -13,7 +13,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"mal/internal/database" "mal/internal/db"
) )
var ( var (

View File

@@ -3,7 +3,7 @@ package auth
import ( import (
"net/http" "net/http"
"mal/internal/templates" "mal/web/templates"
) )
type Handler struct { type Handler struct {

View File

@@ -14,10 +14,10 @@ import (
"strings" "strings"
"time" "time"
"mal/internal/database" "mal/internal/db"
"mal/internal/jikan" "mal/integrations/jikan"
"mal/internal/shared/middleware" "mal/internal/middleware"
"mal/internal/templates" "mal/web/templates"
) )
type Handler struct { type Handler struct {

View File

@@ -9,7 +9,7 @@ import (
"github.com/google/uuid" "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 { func (s *Service) SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64, animeSeed *database.UpsertAnimeParams) error {

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"testing" "testing"
"mal/internal/database" "mal/internal/db"
) )
func TestNormalizeProxyURLRejectsLocalhost(t *testing.T) { func TestNormalizeProxyURLRejectsLocalhost(t *testing.T) {

View File

@@ -5,7 +5,7 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"mal/internal/database" "mal/internal/db"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"

View File

@@ -8,9 +8,9 @@ import (
"slices" "slices"
"strconv" "strconv"
"mal/internal/database" "mal/internal/db"
"mal/internal/shared/middleware" "mal/internal/middleware"
"mal/internal/templates" "mal/web/templates"
) )
type Handler struct { type Handler struct {

View File

@@ -10,7 +10,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"mal/internal/database" "mal/internal/db"
) )
type Service struct { type Service struct {

View File

@@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"mal/internal/database" "mal/internal/db"
) )
type fakeQuerier struct { type fakeQuerier struct {

View File

@@ -14,11 +14,11 @@ import (
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"mal/internal/database" dbpkg "mal/internal/db"
"mal/internal/features/auth" "mal/api/auth"
"mal/internal/jikan" "mal/integrations/jikan"
"mal/internal/server" "mal/internal/server"
"mal/internal/shared/middleware" "mal/pkg/middleware"
"mal/internal/worker" "mal/internal/worker"
) )
@@ -30,11 +30,11 @@ func main() {
defer db.Close() defer db.Close()
migrationsDir := migrationsDir() 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) log.Fatalf("failed to run migrations: %v", err)
} }
queries := database.New(db) queries := dbpkg.New(db)
jikanClient := jikan.NewClient(queries) jikanClient := jikan.NewClient(queries)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)

View File

@@ -13,7 +13,7 @@ import (
"sync" "sync"
"time" "time"
"mal/internal/database" "mal/internal/db"
) )
type Client struct { type Client struct {

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"mal/internal/watchorder" "mal/integrations/watchorder"
) )
const chiakiWatchOrderURL = "https://chiaki.site/?/tools/watch_order/id/%d" const chiakiWatchOrderURL = "https://chiaki.site/?/tools/watch_order/id/%d"

View File

@@ -7,7 +7,7 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"mal/internal/database" "mal/internal/db"
) )
type staleCacheQuerier struct { type staleCacheQuerier struct {

View File

@@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"mal/internal/database" "mal/internal/db"
) )
type AccessPolicy struct { type AccessPolicy struct {

View File

@@ -5,8 +5,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"mal/internal/database" "mal/internal/db"
"mal/internal/features/auth" "mal/api/auth"
) )
type contextKey string type contextKey string

View File

@@ -4,13 +4,14 @@ import (
"database/sql" "database/sql"
"net/http" "net/http"
"mal/internal/database" "mal/internal/db"
"mal/internal/features/anime" "mal/api/anime"
"mal/internal/features/auth" "mal/api/auth"
"mal/internal/features/playback" "mal/api/playback"
"mal/internal/features/watchlist" "mal/api/watchlist"
"mal/internal/jikan" "mal/integrations/jikan"
"mal/internal/shared/middleware" "mal/internal/middleware"
pkgmiddleware "mal/pkg/middleware"
) )
type Config struct { type Config struct {
@@ -70,7 +71,7 @@ func NewRouter(cfg Config) http.Handler {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
authHandler.HandleLoginPage(w, r) authHandler.HandleLoginPage(w, r)
} else { } 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, // Wrap mux with global CSRF origin verification and auth checking,
// THEN auth context parsing. // 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) authenticatedHandler := middleware.Auth(cfg.AuthService)(protectedHandler)
return middleware.RequestLogger(authenticatedHandler) return pkgmiddleware.RequestLogger(authenticatedHandler)
} }

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -7,8 +7,8 @@ import (
"log" "log"
"time" "time"
"mal/internal/database" "mal/internal/db"
"mal/internal/jikan" "mal/integrations/jikan"
) )
type Worker struct { type Worker struct {