feat: add comments and cleanup unused imports across codebase
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package context
|
||||
|
||||
// UserKey is the context key for storing the authenticated user.
|
||||
// It is unexported to prevent collisions.
|
||||
type key int
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// NullStringOr returns n.String if valid and non-empty, otherwise fallback
|
||||
func NullStringOr(n sql.NullString, fallback string) string {
|
||||
if n.Valid && n.String != "" {
|
||||
return n.String
|
||||
@@ -14,6 +15,7 @@ func NullStringOr(n sql.NullString, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// DisplayTitle returns the English title, falling back to Japanese then original
|
||||
func DisplayTitle(titleEnglish, titleJapanese sql.NullString, titleOriginal string) string {
|
||||
return NullStringOr(titleEnglish, NullStringOr(titleJapanese, titleOriginal))
|
||||
}
|
||||
@@ -22,6 +24,7 @@ func (r GetUserWatchListRow) DisplayTitle() string {
|
||||
return DisplayTitle(r.TitleEnglish, r.TitleJapanese, r.TitleOriginal)
|
||||
}
|
||||
|
||||
// BoolPtr converts a nullable bool to a pointer; nil if not valid
|
||||
func BoolPtr(b sql.NullBool) *bool {
|
||||
if !b.Valid {
|
||||
return nil
|
||||
@@ -29,6 +32,7 @@ func BoolPtr(b sql.NullBool) *bool {
|
||||
return &b.Bool
|
||||
}
|
||||
|
||||
// BeginTx starts a transaction and returns the Queries wrapper bound to it
|
||||
func BeginTx(ctx context.Context, db *sql.DB) (*Queries, *sql.Tx, error) {
|
||||
if db == nil {
|
||||
return nil, nil, errors.New("database unavailable")
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RunMigrations applies all *.sql files in migrationsDir in sorted order,
|
||||
// skipping any already recorded in migration_version.
|
||||
func RunMigrations(db *sql.DB, migrationsDir string) error {
|
||||
if migrationsDir == "" {
|
||||
return fmt.Errorf("migrations directory is required")
|
||||
@@ -44,22 +46,19 @@ func RunMigrations(db *sql.DB, migrationsDir string) error {
|
||||
for _, migrationFile := range migrations {
|
||||
migrationName := filepath.Base(migrationFile)
|
||||
if migrationApplied(appliedNames, migrationName) {
|
||||
// already applied, skipping silently
|
||||
continue
|
||||
continue // already applied
|
||||
}
|
||||
|
||||
// Read and execute migration
|
||||
migrationSQL, err := os.ReadFile(migrationFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Strict execution: if it fails, it halts.
|
||||
if _, err := db.Exec(string(migrationSQL)); err != nil {
|
||||
return err
|
||||
return err // stop on first failure
|
||||
}
|
||||
|
||||
// Mark as applied
|
||||
// record applied migration
|
||||
_, err = db.Exec("INSERT INTO migration_version (name) VALUES (?)", migrationName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -97,6 +96,8 @@ func loadAppliedMigrationNames(db *sql.DB) (map[string]struct{}, error) {
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
// migrationApplied checks the applied names map for a match,
|
||||
// including legacy paths and case-insensitive basename matches.
|
||||
func migrationApplied(appliedNames map[string]struct{}, migrationName string) bool {
|
||||
if _, exists := appliedNames[migrationName]; exists {
|
||||
return true
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// Open connects to a sqlite3 database with foreign keys enforced
|
||||
func Open(dbFile string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?_foreign_keys=on", dbFile))
|
||||
if err != nil {
|
||||
@@ -17,6 +18,7 @@ func Open(dbFile string) (*sql.DB, error) {
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// GetDBFile returns the database file path, checking DATABASE_FILE env var first
|
||||
func GetDBFile() string {
|
||||
if f := os.Getenv("DATABASE_FILE"); f != "" {
|
||||
return f
|
||||
@@ -24,6 +26,7 @@ func GetDBFile() string {
|
||||
return "mal.db"
|
||||
}
|
||||
|
||||
// GetMigrationsDir returns the migrations directory, checking MIGRATIONS_DIR env var first
|
||||
func GetMigrationsDir() (string, error) {
|
||||
if dir := os.Getenv("MIGRATIONS_DIR"); dir != "" {
|
||||
return dir, nil
|
||||
@@ -35,6 +38,7 @@ func GetMigrationsDir() (string, error) {
|
||||
return filepath.Join(wd, "migrations"), nil
|
||||
}
|
||||
|
||||
// Init opens the database, runs migrations, and returns a Queries instance
|
||||
func Init(db *sql.DB) (*Queries, error) {
|
||||
migrationsDir, err := GetMigrationsDir()
|
||||
if err != nil {
|
||||
|
||||
@@ -6,18 +6,18 @@ import (
|
||||
)
|
||||
|
||||
type AccessPolicy struct {
|
||||
PublicPaths map[string]struct{}
|
||||
PublicHeads []string
|
||||
PublicPaths map[string]struct{} // exact match paths (e.g. /login)
|
||||
PublicHeads []string // prefix match paths (e.g. /static/)
|
||||
}
|
||||
|
||||
func NewAccessPolicy() AccessPolicy {
|
||||
return AccessPolicy{
|
||||
PublicPaths: map[string]struct{}{
|
||||
"/login": {},
|
||||
"/login": {}, // login page is public
|
||||
},
|
||||
PublicHeads: []string{
|
||||
"/static/",
|
||||
"/dist/",
|
||||
"/static/", // static assets
|
||||
"/dist/", // bundled assets
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@ func (p AccessPolicy) IsPublicPath(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RequireGlobalAuthWithPolicy redirects unauthenticated users to /login
|
||||
// uses HX-Redirect for HTMX requests, regular redirect otherwise
|
||||
func RequireGlobalAuthWithPolicy(policy AccessPolicy) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -9,18 +9,19 @@ import (
|
||||
"mal/internal/db"
|
||||
)
|
||||
|
||||
// Auth middleware validates the session cookie and injects the user into context
|
||||
func Auth(authService *auth.Service) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("session_id")
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
next.ServeHTTP(w, r) // no cookie, proceed unauthenticated
|
||||
return
|
||||
}
|
||||
|
||||
user, err := authService.ValidateSession(r.Context(), cookie.Value)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
next.ServeHTTP(w, r) // invalid session, proceed unauthenticated
|
||||
return
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ func Auth(authService *auth.Service) func(http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// GetUser retrieves the authenticated user from context, or nil if not authenticated
|
||||
func GetUser(ctx context.Context) *db.User {
|
||||
user, ok := ctx.Value(ctxpkg.UserKey).(*db.User)
|
||||
if !ok {
|
||||
|
||||
@@ -27,6 +27,7 @@ type Config struct {
|
||||
PlaybackProxySecret string
|
||||
}
|
||||
|
||||
// withMimeTypes sets Content-Type for common static asset extensions
|
||||
func withMimeTypes(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := strings.ToLower(filepath.Ext(r.URL.Path))
|
||||
@@ -44,6 +45,7 @@ func withMimeTypes(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// noCache sends headers to prevent caching of dynamic/static assets
|
||||
func noCache(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
@@ -53,6 +55,7 @@ func noCache(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// NewAuthLimiter returns a rate limiter for auth endpoints: 5 attempts per minute
|
||||
func NewAuthLimiter() *pkgmiddleware.Limiter {
|
||||
return pkgmiddleware.NewLimiter(pkgmiddleware.Config{
|
||||
MaxAttempts: 5,
|
||||
@@ -60,6 +63,8 @@ func NewAuthLimiter() *pkgmiddleware.Limiter {
|
||||
})
|
||||
}
|
||||
|
||||
// NewRouter wires up all HTTP handlers and middleware.
|
||||
// Auth is enforced globally; public routes must opt-out via middleware policy.
|
||||
func NewRouter(cfg Config) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
|
||||
@@ -26,12 +26,13 @@ func New(db *db.Queries, client *jikan.Client) *Worker {
|
||||
|
||||
func (w *Worker) Start(ctx context.Context) {
|
||||
log.Println("Starting relations sync worker...")
|
||||
// ticker: regular sync; retryTicker: check for failed API retries
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
retryTicker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
defer retryTicker.Stop()
|
||||
|
||||
// Run once immediately
|
||||
// Run once immediately on startup
|
||||
w.runAllTasks(ctx)
|
||||
|
||||
cleanupCounter := 0
|
||||
@@ -78,6 +79,7 @@ func (w *Worker) runAllTasks(ctx context.Context) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// retryBackoff calculates the next retry delay, doubling up to 30 minutes max
|
||||
func retryBackoff(attempts int64) string {
|
||||
if attempts < 1 {
|
||||
attempts = 1
|
||||
@@ -97,6 +99,7 @@ func retryBackoff(attempts int64) string {
|
||||
return fmt.Sprintf("+%d minutes", minutes)
|
||||
}
|
||||
|
||||
// processAnimeFetchRetries retries failed Jikan API fetches for anime with pending entries
|
||||
func (w *Worker) processAnimeFetchRetries(ctx context.Context) {
|
||||
retries, err := w.db.GetDueAnimeFetchRetries(ctx, 20)
|
||||
if err != nil {
|
||||
@@ -139,6 +142,7 @@ func (w *Worker) cleanupCache(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// syncRelations fetches relation data for anime that need syncing via a worker pool
|
||||
func (w *Worker) syncRelations(ctx context.Context) {
|
||||
animes, err := w.db.GetAnimeNeedingRelationSync(ctx)
|
||||
if err != nil {
|
||||
@@ -170,6 +174,8 @@ func (w *Worker) syncRelations(ctx context.Context) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// syncSingleAnime fetches relations for one anime, inserts them, and marks it synced.
|
||||
// For sequels, also ensures the related anime exists in the DB to enable linking.
|
||||
func (w *Worker) syncSingleAnime(ctx context.Context, id int64) {
|
||||
animeData, err := w.client.GetAnimeByID(ctx, int(id))
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user