Phase 6: Permissions, Push, WebAuthn scaffolding

Backend:
- internal/permissions/permissions.go: permission bitflags (VIEW_CHANNEL through SHARE_SCREEN)
- internal/permissions/checker.go: permission checker with role aggregation
- internal/middleware/permission.go: RequirePermission middleware
- internal/push/handlers.go: push subscription CRUD, VAPID key endpoint, SendPush
- internal/auth/webauthn.go: WebAuthn passkey scaffolding (begin/finish endpoints)
- internal/db/db.go: is_default on roles, push_subscriptions table, webauthn_credentials table
- go.mod: added webpush-go, go-webauthn dependencies

Frontend:
- stores/role.ts: Zustand store for role management
- RoleManager.tsx: role CRUD with permission checkboxes
- MemberRoleAssign.tsx: assign roles to members
- App.tsx: /servers/:serverId/roles route
This commit is contained in:
2026-06-28 17:53:44 -04:00
parent 1db0c3b37a
commit af1de3d140
14 changed files with 1232 additions and 4 deletions
+123
View File
@@ -0,0 +1,123 @@
package auth
import (
"database/sql"
"encoding/json"
"log/slog"
"net/http"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
"github.com/go-webauthn/webauthn/webauthn"
)
type WebAuthnHandler struct {
db *sql.DB
wa *webauthn.WebAuthn
sessions *SessionStore
logger *slog.Logger
}
func NewWebAuthnHandler(db *sql.DB, cfg *config.Config, sessions *SessionStore, logger *slog.Logger) (*WebAuthnHandler, error) {
wa, err := webauthn.New(&webauthn.Config{
RPDisplayName: "Dumpster",
RPID: cfg.Host,
RPOrigins: []string{"https://" + cfg.Host, "http://" + cfg.Host + ":" + cfg.Port},
})
if err != nil {
return nil, err
}
return &WebAuthnHandler{
db: db,
wa: wa,
sessions: sessions,
logger: logger,
}, nil
}
func (h *WebAuthnHandler) RegisterRoutes(r *http.ServeMux) {
r.HandleFunc("POST /auth/webauthn/register/begin", h.RegisterBegin)
r.HandleFunc("POST /auth/webauthn/register/finish", h.RegisterFinish)
r.HandleFunc("POST /auth/webauthn/login/begin", h.LoginBegin)
r.HandleFunc("POST /auth/webauthn/login/finish", h.LoginFinish)
}
func (h *WebAuthnHandler) RegisterBegin(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
var user WebAuthnUser
err := h.db.QueryRowContext(r.Context(),
`SELECT id, username, display_name FROM users WHERE id = $1`, userID,
).Scan(&user.ID, &user.Username, &user.DisplayName)
if err != nil {
http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
return
}
// Get existing credentials
rows, _ := h.db.QueryContext(r.Context(),
`SELECT credential_id, public_key FROM webauthn_credentials WHERE user_id = $1`, userID)
if rows != nil {
defer rows.Close()
for rows.Next() {
var credID, pubKey []byte
rows.Scan(&credID, &pubKey)
user.credentials = append(user.credentials, webauthn.Credential{
ID: credID,
PublicKey: pubKey,
})
}
}
options, sessionData, err := h.wa.BeginRegistration(&user)
if err != nil {
http.Error(w, `{"error":"registration failed"}`, http.StatusInternalServerError)
return
}
// Store session data (simplified: in production use a proper session store)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(options)
_ = sessionData // TODO: store in session for finish step
}
func (h *WebAuthnHandler) RegisterFinish(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
// TODO: implement full registration finish with session data
http.Error(w, `{"error":"not implemented"}`, http.StatusNotImplemented)
_ = userID
}
func (h *WebAuthnHandler) LoginBegin(w http.ResponseWriter, r *http.Request) {
// TODO: implement login begin
http.Error(w, `{"error":"not implemented"}`, http.StatusNotImplemented)
}
func (h *WebAuthnHandler) LoginFinish(w http.ResponseWriter, r *http.Request) {
// TODO: implement login finish
http.Error(w, `{"error":"not implemented"}`, http.StatusNotImplemented)
}
// WebAuthnUser implements the webauthn.User interface
type WebAuthnUser struct {
ID string
Username string
DisplayName string
credentials []webauthn.Credential
}
func (u *WebAuthnUser) WebAuthnID() []byte { return []byte(u.ID) }
func (u *WebAuthnUser) WebAuthnName() string { return u.Username }
func (u *WebAuthnUser) WebAuthnDisplayName() string { return u.DisplayName }
func (u *WebAuthnUser) WebAuthnIcon() string { return "" }
func (u *WebAuthnUser) WebAuthnCredentials() []webauthn.Credential { return u.credentials }