refactor: reorganize project structure following go standards
This commit is contained in:
@@ -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 {
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"mal/internal/database"
|
"mal/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -3,7 +3,7 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"mal/internal/templates"
|
"mal/web/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -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) {
|
||||||
@@ -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"
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"mal/internal/database"
|
"mal/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeQuerier struct {
|
type fakeQuerier struct {
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"mal/internal/database"
|
"mal/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -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"
|
||||||
@@ -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 {
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"mal/internal/database"
|
"mal/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccessPolicy struct {
|
type AccessPolicy struct {
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user