first commit

This commit is contained in:
2026-06-22 16:06:57 +02:00
commit fe485dd86d
90 changed files with 11404 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
package meta
import (
"context"
"nadir"
"nadir/internal/auth"
"github.com/danielgtaylor/huma/v2"
)
type HealthOutput struct {
Body struct {
Status string `json:"status" example:"ok" doc:"Overall health"`
Database string `json:"database" example:"ok" doc:"Embedded SQLite session store state"`
Version string `json:"version" example:"1.0.0" doc:"Application version"`
}
}
// RegisterHealth adds a public liveness/readiness probe. It is intentionally
// unauthenticated (no permission metadata) so load balancers and orchestrators
// can reach it. Returns 503 when the SQLite session store is unreachable, so
// probes can key off the status code without parsing the body.
func RegisterHealth(api huma.API, sessions *auth.SessionStore) {
huma.Register(api, huma.Operation{
OperationID: "health",
Method: "GET",
Path: "/api/health",
Summary: "Health check",
Description: "Public liveness/readiness probe. Reports whether the embedded " +
"SQLite session store is reachable. Returns 503 when it is not.",
Tags: []string{"Meta"},
Errors: []int{503},
}, func(ctx context.Context, _ *struct{}) (*HealthOutput, error) {
if err := sessions.Ping(); err != nil {
return nil, huma.Error503ServiceUnavailable("session database unreachable", err)
}
out := &HealthOutput{}
out.Body.Status = "ok"
out.Body.Database = "ok"
out.Body.Version = nadir.Version
return out, nil
})
}
+58
View File
@@ -0,0 +1,58 @@
package meta
import (
"cmp"
"context"
"slices"
"nadir/internal/module"
"github.com/danielgtaylor/huma/v2"
)
// ModuleInfo describes a registered module: its stable ID, display name, and
// the permissions it exposes. The frontend uses this to drive navigation and
// render the role/permission matrix.
type ModuleInfo struct {
ID string `json:"id" example:"system" doc:"Stable module identifier"`
Name string `json:"name" example:"System" doc:"Human-readable module name"`
Permissions []string `json:"permissions" doc:"Permissions this module exposes (never includes the \"*\" wildcard)"`
}
type ModulesOutput struct {
Body struct {
Modules []ModuleInfo `json:"modules" doc:"Registered modules, sorted by ID"`
}
}
// Register adds the read-only module-discovery endpoint. It is intentionally
// public: it exposes only the API's static shape (module IDs and permission
// vocabulary), the same information already served by /openapi.json. The module
// list is fixed at startup, so the response is computed once here.
func Register(api huma.API, mods []module.Module) {
infos := make([]ModuleInfo, 0, len(mods))
for _, m := range mods {
perms := m.Permissions()
ps := make([]string, len(perms))
for i, p := range perms {
ps[i] = string(p)
}
infos = append(infos, ModuleInfo{ID: m.ID(), Name: m.Name(), Permissions: ps})
}
slices.SortFunc(infos, func(a, b ModuleInfo) int { return cmp.Compare(a.ID, b.ID) })
huma.Register(api, huma.Operation{
OperationID: "list-modules",
Method: "GET",
Path: "/api/_modules",
Summary: "List registered modules",
Description: "Returns every registered module with its ID, display name, " +
"and exported permissions. Public (same static shape as /openapi.json); " +
"used by the frontend for navigation and the role/permission matrix.",
Tags: []string{"Meta"},
}, func(ctx context.Context, _ *struct{}) (*ModulesOutput, error) {
out := &ModulesOutput{}
out.Body.Modules = infos
return out, nil
})
}
+67
View File
@@ -0,0 +1,67 @@
package meta
import (
"context"
"nadir/internal/auth"
"nadir/internal/module"
"nadir/internal/rbac"
"github.com/danielgtaylor/huma/v2"
)
// WhoamiInput carries the session cookie. The endpoint is not behind the RBAC
// middleware (it requires no specific permission), so it validates the session
// itself.
type WhoamiInput struct {
SessionID string `cookie:"nadir_session_id"`
}
// WhoamiBody reports who the caller is and, per module, which permissions they
// actually hold. Combined with /api/_modules (the full module/permission grid),
// this gives the frontend everything it needs to render the permission matrix.
type WhoamiBody struct {
Username string `json:"username" example:"urania" doc:"Authenticated username"`
Permissions map[string][]string `json:"permissions" doc:"Module ID -> permissions the caller holds. Modules where they hold none are omitted."`
}
type WhoamiOutput struct{ Body WhoamiBody }
// RegisterWhoami adds the current-user endpoint. It resolves the caller's
// concrete grants by asking the RBAC store about each module's permissions,
// so "*" wildcards in roles are expanded for free.
func RegisterWhoami(api huma.API, sessions *auth.SessionStore, roles *rbac.RBAC, mods []module.Module) {
huma.Register(api, huma.Operation{
OperationID: "whoami",
Method: "GET",
Path: "/api/whoami",
Summary: "Get the current user and their permissions",
Description: "Returns the authenticated username and, per module, the " +
"permissions the caller holds (wildcards resolved). Pair with " +
"/api/_modules to render the full permission matrix.",
Tags: []string{"Meta"},
Errors: []int{401},
}, func(ctx context.Context, in *WhoamiInput) (*WhoamiOutput, error) {
sess, ok := sessions.GetByToken(in.SessionID)
if !ok {
return nil, huma.Error401Unauthorized("unauthorized")
}
held := make(map[string][]string)
for _, m := range mods {
var perms []string
for _, p := range m.Permissions() {
if roles.Can(sess.Username, m.ID(), p) {
perms = append(perms, string(p))
}
}
if len(perms) > 0 {
held[m.ID()] = perms
}
}
out := &WhoamiOutput{}
out.Body = WhoamiBody{Username: sess.Username, Permissions: held}
return out, nil
})
}