package api import ( "log/slog" "net/http" "strings" "atmoquest/internal/connection" "atmoquest/internal/users" ) // sessionResponse is the JSON shape for GET /api/v1/auth/session. type sessionResponse struct { DID string `json:"did"` Handle string `json:"handle"` IsAdmin bool `json:"isAdmin"` } // GetSession returns the current authenticated user's session info. // GET /api/v1/auth/session func (h *Handlers) GetSession(w http.ResponseWriter, r *http.Request) { did, ok := h.requireAuth(w, r) if !ok { return } handle := h.resolveHandle(r, did.String()) isAdmin, _ := users.IsAdmin(r.Context(), h.DB, did) writeJSON(w, http.StatusOK, sessionResponse{ DID: did.String(), Handle: handle, IsAdmin: isAdmin, }) } // OAuthLogin starts the OAuth flow for mobile clients. // POST /api/v1/oauth/login // // Request: {"handle": "alice.bsky.social"} // Response: {"authUrl": "https://...", "state": "..."} func (h *Handlers) OAuthLogin(w http.ResponseWriter, r *http.Request) { var body struct { Handle string `json:"handle"` } if err := decodeBody(r, &body); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } identifier := strings.TrimSpace(body.Handle) if identifier == "" { writeError(w, http.StatusBadRequest, "handle is required") return } redirectURL, err := h.Auth.OAuth.StartAuthFlow(r.Context(), identifier) if err != nil { slog.Warn("api: oauth start", "identifier", identifier, "err", err, ) writeError(w, http.StatusBadRequest, "couldn't start sign-in") return } writeJSON(w, http.StatusOK, map[string]string{ "authUrl": redirectURL, }) } // OAuthCallback completes the OAuth flow for mobile clients. // GET /api/v1/oauth/callback?code=...&state=... // // Response: {"token": "...", "did": "...", "handle": "..."} func (h *Handlers) OAuthCallback(w http.ResponseWriter, r *http.Request) { sessData, err := h.Auth.OAuth.ProcessCallback(r.Context(), r.URL.Query()) if err != nil { slog.Warn("api: oauth callback", "err", err) writeError(w, http.StatusBadRequest, "sign-in failed") return } // Issue a Bearer token for the mobile app. token := h.Tokens.Issue(sessData.AccountDID.String(), sessData.SessionID) // Best-effort: record login in users table. go h.Auth.RecordUserLogin(sessData) // Best-effort: drain pending connections. if h.ConnQueue != nil { sess, err := h.Auth.OAuth.ResumeSession(r.Context(), sessData.AccountDID, sessData.SessionID) if err == nil { res, err := connection.Drain(r.Context(), h.ConnQueue, sess, sess.Data.HostURL, slog.Default(), nil) if err != nil { slog.Warn("api: connect drain", "did", sessData.AccountDID.String(), "err", err) } else if res.Written > 0 || res.Skipped > 0 { slog.Info("api: connect drain", "did", sessData.AccountDID.String(), "written", res.Written, "skipped", res.Skipped) } } } handle := h.resolveHandle(r, sessData.AccountDID.String()) writeJSON(w, http.StatusOK, map[string]string{ "token": token, "did": sessData.AccountDID.String(), "handle": handle, }) } // Logout revokes the current session. // POST /api/v1/oauth/logout func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request) { h.Auth.Sessions.Clear(w, r) writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) }