package main import ( "errors" "net/http" "time" "fn-registry/functions/infra" ) const ( cookieName = "kanban_session" sessionTTL = 7 * 24 * time.Hour ) type ctxKey string const userCtxKey ctxKey = "kanban_user_id" func setSessionCookie(w http.ResponseWriter, token string, expiresAt int64) { infra.SessionCookieSet(w, cookieName, token, expiresAt) } func clearSessionCookie(w http.ResponseWriter) { infra.SessionCookieClear(w, cookieName) } func tokenFromRequest(r *http.Request) string { return infra.SessionTokenExtract(r, cookieName) } // POST /api/auth/register {username, password, display_name?} func handleRegister(db *DB, flags *FeatureFlags) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !flags.Enabled("registration-enabled") { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusForbidden, Code: "registration_disabled", Message: "user registration is disabled on this instance"}) return } var body struct { Username string `json:"username"` Password string `json:"password"` DisplayName string `json:"display_name"` } if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil { badRequest(w, err.Error()) return } u, err := db.CreateUser(body.Username, body.Password, body.DisplayName) if err != nil { if errors.Is(err, errUserAlreadyExists) { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusConflict, Code: "user_exists", Message: err.Error()}) return } badRequest(w, err.Error()) return } infra.HTTPJSONResponse(w, http.StatusCreated, u) } } // POST /api/auth/login {username, password} func handleLogin(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var body struct { Username string `json:"username"` Password string `json:"password"` } if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil { badRequest(w, err.Error()) return } u, err := db.Authenticate(body.Username, body.Password) if err != nil { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusUnauthorized, Code: "invalid_credentials", Message: "invalid username or password"}) return } sess, err := infra.SessionCreate(db.conn, u.ID, sessionTTL, map[string]any{"username": u.Username}) if err != nil { serverError(w, err) return } setSessionCookie(w, sess.Token, sess.ExpiresAt) infra.HTTPJSONResponse(w, http.StatusOK, u) } } // POST /api/auth/logout func handleLogout(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := tokenFromRequest(r) if token != "" { _ = db.DeleteSessionByToken(token) } clearSessionCookie(w) w.WriteHeader(http.StatusNoContent) } } // GET /api/me func handleMe(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, ok := infra.UserIDFromContext(r.Context(), userCtxKey) if !ok { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "no session"}) return } u, err := db.GetUserByID(uid) if err != nil { serverError(w, err) return } infra.HTTPJSONResponse(w, http.StatusOK, u) } } // PATCH /api/me { color? } func handlePatchMe(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, ok := infra.UserIDFromContext(r.Context(), userCtxKey) if !ok { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "no session"}) return } var body struct { Color *string `json:"color"` } if err := infra.HTTPParseBody(r, &body, maxBodyBytes); err != nil { badRequest(w, err.Error()) return } if body.Color != nil { if err := db.UpdateUserColor(uid, *body.Color); err != nil { serverError(w, err) return } } u, err := db.GetUserByID(uid) if err != nil { serverError(w, err) return } infra.HTTPJSONResponse(w, http.StatusOK, u) } } // GET /api/users func handleListUsers(db *DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { users, err := db.ListUsers() if err != nil { serverError(w, err) return } infra.HTTPJSONResponse(w, http.StatusOK, users) } }