feat: auth base

This commit is contained in:
Elisiei Yehorov 2026-04-19 01:40:26 +02:00
parent 434f5a82bb
commit 55751db156
Signed by: elisiei
GPG key ID: BA1D158DCE3DF089
6 changed files with 145 additions and 21 deletions

View file

@ -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)
})
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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) {

65
services/auth/auth.go Normal file
View file

@ -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
}

68
services/auth/router.go Normal file
View file

@ -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)
}