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:
@@ -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 }
|
||||
Reference in New Issue
Block a user