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"
"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 {

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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"
"time"
"mal/internal/database"
"mal/internal/jikan"
"mal/internal/db"
"mal/integrations/jikan"
)
type Worker struct {