From 55751db1562ea3cd27c26f2c619ee90c0f382e66 Mon Sep 17 00:00:00 2001 From: Elisiei Yehorov Date: Sun, 19 Apr 2026 01:40:26 +0200 Subject: [PATCH] feat: auth base --- internal/middleware/auth.go | 19 ++--------- internal/server/server.go | 10 ++++-- main.go | 2 +- services/admin/router.go | 2 ++ services/auth/auth.go | 65 +++++++++++++++++++++++++++++++++++ services/auth/router.go | 68 +++++++++++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 services/auth/auth.go create mode 100644 services/auth/router.go diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index dd469b5..5f2adab 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -13,29 +13,14 @@ type contextKey string const userKey contextKey = "user" -var mainUserID = "774287684944134155" - // AuthMiddleware is a middleware to set the user as context value. // this middleware does not prevents the user from accessing the route // if not authorized. func AuthMiddleware(q *db.Queries) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user, err := q.GetUser(r.Context(), mainUserID) - if err != nil { - user, _ = q.CreateUser(r.Context(), db.CreateUserParams{ - ID: mainUserID, - Username: "elisiei", - }) - } - - q.CreateUser(r.Context(), db.CreateUserParams{ - ID: "740358234002686004", - Username: "ulysses_ck", - }) //test - - ctx := context.WithValue(r.Context(), userKey, user) // mocked - next.ServeHTTP(w, r.WithContext(ctx)) + // ctx := context.WithValue(r.Context(), userKey, user) // mocked + next.ServeHTTP(w, r) }) } } diff --git a/internal/server/server.go b/internal/server/server.go index a93709e..977f77b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -11,6 +11,7 @@ import ( "codeberg.org/nextgo/dbots/internal/discord" customMiddlewares "codeberg.org/nextgo/dbots/internal/middleware" "codeberg.org/nextgo/dbots/services/admin" + "codeberg.org/nextgo/dbots/services/auth" "codeberg.org/nextgo/dbots/services/bot" "github.com/go-chi/chi/v5" @@ -47,17 +48,20 @@ func (s *Server) Register() { s.config.Discord.ClientSecret, s.config.Discord.RedirectURI, ) + + authRouter := auth.NewRouter(s.queries, discordClient) botRouter := bot.NewRouter(s.queries, discordClient) adminRouter := admin.NewRouter(s.queries) + s.router.Mount("/auth", authRouter.Routes()) s.router.Mount("/bots", botRouter.Routes()) s.router.Mount("/admin", adminRouter.Routes()) } -func (s *Server) Start(port int) { +func (s *Server) Start(addr string, port int) { s.Register() - slog.Info("server started", "port", port) - if err := http.ListenAndServe(fmt.Sprintf(":%d", port), s.router); err != nil { + slog.Info("server started", "addr", addr, "port", port) + if err := http.ListenAndServe(fmt.Sprintf("%s:%d", addr, port), s.router); err != nil { slog.Error("error starting server", "port", port, "err", err) os.Exit(1) } diff --git a/main.go b/main.go index c691bb7..883b390 100644 --- a/main.go +++ b/main.go @@ -31,5 +31,5 @@ func main() { queries := db.New(conn) server := server.NewServer(queries, config) - server.Start(config.Server.Port) + server.Start(config.Server.Address, config.Server.Port) } diff --git a/services/admin/router.go b/services/admin/router.go index c198b1c..7d2ca2f 100644 --- a/services/admin/router.go +++ b/services/admin/router.go @@ -26,6 +26,8 @@ func NewRouter(q *db.Queries) *Router { } func (r *Router) Routes() http.Handler { + r.router.Use(middleware.AuthGuardMiddleware) // todo: admin middleware + r.router.Route("/bots", func(router chi.Router) { router.Get("/", r.listBots) router.Route("/{botID}", func(b chi.Router) { diff --git a/services/auth/auth.go b/services/auth/auth.go new file mode 100644 index 0000000..76f9a15 --- /dev/null +++ b/services/auth/auth.go @@ -0,0 +1,65 @@ +package auth + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + + "codeberg.org/nextgo/dbots/internal/db" + "codeberg.org/nextgo/dbots/internal/discord" + "github.com/jackc/pgx/v5" +) + +// todo: api keysssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss +// or sessions???????????? +type Service struct { + q *db.Queries + client *discord.Client +} + +func NewService(q *db.Queries, client *discord.Client) *Service { + return &Service{q: q, client: client} +} + +// GenerateState produces a random OAuth state parameter. +func GenerateState() (string, error) { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + +// Callback handles the OAuth callback: exchanges the code, fetches the user, +// and upserts them in the database. Returns the local db.User. +func (s *Service) Callback(ctx context.Context, code string) (*db.User, error) { + token, err := s.client.ExchangeCode(ctx, code) + if err != nil { + return nil, err + } + + dUser, err := s.client.GetCurrentUser(ctx, token.AccessToken) + if err != nil { + return nil, err + } + + user, err := s.q.UpdateUser(ctx, db.UpdateUserParams{ + ID: dUser.ID, + Username: &dUser.Username, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + user, err = s.q.CreateUser(ctx, db.CreateUserParams{ + ID: dUser.ID, + Username: dUser.Username, + }) + if err != nil { + return nil, err + } + } + return nil, err + } + + return user, nil +} diff --git a/services/auth/router.go b/services/auth/router.go new file mode 100644 index 0000000..647509f --- /dev/null +++ b/services/auth/router.go @@ -0,0 +1,68 @@ +package auth + +import ( + "net/http" + + "codeberg.org/nextgo/dbots/internal/db" + "codeberg.org/nextgo/dbots/internal/discord" + "codeberg.org/nextgo/dbots/internal/errorutil" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +type Router struct { + auth *Service + router chi.Router +} + +func NewRouter(q *db.Queries, client *discord.Client) *Router { + return &Router{ + auth: NewService(q, client), + router: chi.NewRouter(), + } +} + +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.Get("/me", r.me) + return r.router +} + +func (r *Router) me(w http.ResponseWriter, req *http.Request) { + +} + +func (r *Router) login(w http.ResponseWriter, req *http.Request) { + state, err := GenerateState() + if err != nil { + render.Render(w, req, errorutil.ErrInternal(err)) + return + } + // todo: store state in a short-lived cookie or session before redirecting + http.Redirect(w, req, r.auth.client.AuthURL(state), http.StatusFound) +} + +func (r *Router) callback(w http.ResponseWriter, req *http.Request) { + // todo: validate state matches what was stored + 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 + } + + // todo: create a session, set a cookie, then redirect to "/" + render.JSON(w, req, user) +} + +func (r *Router) logout(w http.ResponseWriter, req *http.Request) { + // todo: delete session + render.NoContent(w, req) +}