af1de3d140
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
124 lines
3.8 KiB
Go
124 lines
3.8 KiB
Go
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 }
|