package main import ( "flag" "fmt" "html/template" "os" "path/filepath" "nebbet.no/internal/admin" "nebbet.no/internal/admin/auth" "nebbet.no/internal/db" "nebbet.no/internal/server" ) const ( outputDir = "public" dataDir = "data" metaDBPath = "data/meta.db" searchDBPath = "data/search.db" passwordFile = ".passwords" ) func main() { if len(os.Args) < 2 { usage() os.Exit(1) } switch os.Args[1] { case "serve": cmdServe(os.Args[2:]) case "user": cmdUser(os.Args[2:]) default: fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1]) usage() os.Exit(1) } } func cmdServe(args []string) { fs := flag.NewFlagSet("serve", flag.ExitOnError) port := fs.String("port", "8080", "port to listen on") _ = fs.Parse(args) meta, search := mustOpenDBs() defer meta.Close() defer search.Close() adminSrv := admin.NewServer(passwordFile, meta, search, outputDir) engine := adminSrv.Engine() // Load templates for post rendering tmpl, err := template.ParseGlob(filepath.Join("templates", "*.html")) if err != nil { fmt.Fprintln(os.Stderr, "template load error:", err) os.Exit(1) } // Define MIME types for each directory libMIMEs := server.AllowedMIMEs{ ".js": "application/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".wasm": "application/wasm", } mediaMIMEs := server.AllowedMIMEs{ ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".gif": "image/gif", ".svg": "image/svg+xml", ".mp4": "video/mp4", } publicMIMEs := server.AllowedMIMEs{ ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".json": "application/json", ".xml": "application/xml", ".txt": "text/plain; charset=utf-8", ".ico": "image/x-icon", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".webp": "image/webp", } stylesMIMEs := server.AllowedMIMEs{ ".css": "text/css; charset=utf-8", } // Serve styles directory engine.GET("/assets/styles/*filepath", server.FileHandler("assets/styles", stylesMIMEs)) // Serve lib directory (JS modules) engine.GET("/lib/*filepath", server.FileHandler("assets/lib", libMIMEs)) // Serve components directory (JS web components) engine.GET("/assets/components/*filepath", server.FileHandler("assets/components", libMIMEs)) // Serve media directory (images, videos) engine.GET("/media/*filepath", server.FileHandler("assets/media", mediaMIMEs)) // Dynamic frontpage with post listing, tags, and search frontpage := server.NewFrontpageHandler(meta, search, tmpl) engine.GET("/", frontpage.Serve) // Serve posts dynamically with caching (single route, dispatch by extension) postHandler := server.NewPostHandler(meta, tmpl, outputDir) engine.GET("/:slug", postHandler.Serve) // Serve static site as fallback for unmatched routes engine.NoRoute(server.PublicFileHandler(outputDir, publicMIMEs)) addr := ":" + *port fmt.Printf("listening on http://localhost%s\n", addr) fmt.Printf(" admin UI: http://localhost%s/admin/\n", addr) if err := engine.Run(addr); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // ── user ───────────────────────────────────────────────────────────────────── func cmdUser(args []string) { if len(args) == 0 { fmt.Fprintln(os.Stderr, "usage: nebbet user [username]") os.Exit(1) } a := auth.New(passwordFile) switch args[0] { case "add": requireArg(args, 1, "nebbet user add ") if err := a.AddUser(args[1]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println("user added:", args[1]) case "list": users, err := a.ListUsers() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if len(users) == 0 { fmt.Println("(no users)") return } for _, u := range users { fmt.Println(u) } case "delete": requireArg(args, 1, "nebbet user delete ") if err := a.DeleteUser(args[1]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println("user deleted:", args[1]) case "passwd": requireArg(args, 1, "nebbet user passwd ") if err := a.ChangePassword(args[1]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println("password updated:", args[1]) default: fmt.Fprintf(os.Stderr, "unknown user subcommand %q\n", args[0]) os.Exit(1) } } func mustOpenDBs() (*db.MetaDB, *db.SearchDB) { if err := os.MkdirAll(dataDir, 0755); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } meta, err := db.OpenMeta(metaDBPath) if err != nil { fmt.Fprintln(os.Stderr, "meta db:", err) os.Exit(1) } search, err := db.OpenSearch(searchDBPath) if err != nil { fmt.Fprintln(os.Stderr, "search db:", err) os.Exit(1) } return meta, search } func requireArg(args []string, n int, usage string) { if len(args) <= n { fmt.Fprintln(os.Stderr, "usage:", usage) os.Exit(1) } } func usage() { fmt.Fprintln(os.Stderr, `nebbet.no Commands: serve [--port N] serve site + admin UI (default port 8080) user add add user to password file user list list users user delete remove user user passwd change user password`) }