package auth import ( "net/http" "time" "codeberg.org/nextgo/dbots/internal/config" "codeberg.org/nextgo/dbots/internal/db" "codeberg.org/nextgo/dbots/internal/discord" "codeberg.org/nextgo/dbots/internal/errorutil" "codeberg.org/nextgo/dbots/internal/middleware" "codeberg.org/nextgo/dbots/internal/token" "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) const cookieName = "session" type Router struct { auth *Service router chi.Router config config.Config queries *db.Queries } func NewRouter(q *db.Queries, client *discord.Client, config config.Config) *Router { return &Router{ auth: NewService(q, client), router: chi.NewRouter(), config: config, queries: q, } } func (r *Router) Routes() http.Handler { r.router.Get("/login", r.login) r.router.Get("/callback", r.callback) r.router.Post("/logout", r.logout) r.router.With(middleware.AuthGuardMiddleware).Get("/me", r.me) return r.router } func (r *Router) me(w http.ResponseWriter, req *http.Request) { user := middleware.GetUser(req.Context()) if user == nil { render.Render(w, req, errorutil.ErrUnauthorized) return } render.JSON(w, req, user) } func (r *Router) login(w http.ResponseWriter, req *http.Request) { state, err := GenerateState() if err != nil { render.Render(w, req, errorutil.ErrInternal(err)) return } http.SetCookie(w, &http.Cookie{ Name: "oauth_state", Value: state, HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 300, Path: "/", }) http.Redirect(w, req, r.auth.client.AuthURL(state), http.StatusFound) } func (r *Router) callback(w http.ResponseWriter, req *http.Request) { stateCookie, err := req.Cookie("oauth_state") if err != nil || stateCookie.Value != req.URL.Query().Get("state") { render.Render(w, req, errorutil.ErrUnauthorized) return } http.SetCookie(w, &http.Cookie{Name: "oauth_state", MaxAge: -1, Path: "/"}) code := req.URL.Query().Get("code") if code == "" { render.Render(w, req, errorutil.ErrInvalidRequest(nil)) return } user, _, err := r.auth.Callback(req.Context(), code) if err != nil { render.Render(w, req, errorutil.ErrInternal(err)) return } jti, err := GenerateState() if err != nil { render.Render(w, req, errorutil.ErrInternal(err)) return } _, err = r.queries.CreateSession(req.Context(), db.CreateSessionParams{ ID: jti, UserID: user.ID, ExpiresAt: time.Now().Add(token.TokenDuration), }) if err != nil { render.Render(w, req, errorutil.ErrInternal(err)) return } key, err := token.KeyFromHex(r.config.Auth.PasetoKey) if err != nil { render.Render(w, req, errorutil.ErrInternal(err)) return } raw, err := token.IssueToken(key, user.ID, jti) if err != nil { render.Render(w, req, errorutil.ErrInternal(err)) return } http.SetCookie(w, &http.Cookie{ Name: cookieName, Value: raw, HttpOnly: true, SameSite: http.SameSiteStrictMode, MaxAge: int(token.TokenDuration.Seconds()), Path: "/", Secure: r.config.Production, }) http.Redirect(w, req, "/", http.StatusFound) } func (r *Router) logout(w http.ResponseWriter, req *http.Request) { c, err := req.Cookie(cookieName) if err != nil { render.NoContent(w, req) return } key, err := token.KeyFromHex(r.config.Auth.PasetoKey) if err == nil { if claims, err := token.VerifyToken(key, c.Value); err == nil { _ = r.queries.RevokeSession(req.Context(), claims.JTI) } } http.SetCookie(w, &http.Cookie{ Name: cookieName, MaxAge: -1, Path: "/", }) render.NoContent(w, req) }