From 51b7e6b3f9ce8d7d18c1a81f87657d802e0e033e Mon Sep 17 00:00:00 2001 From: LandaMm Date: Fri, 30 May 2025 18:17:12 +0200 Subject: [PATCH] feat: admin routes + better auth routing --- cmd/hspguard/api/api.go | 14 +++---- internal/{apiservices => admin}/routes.go | 51 ++++++++++++----------- internal/auth/routes.go | 21 +++++++--- internal/middleware/admin.go | 47 +++++++++++++++++++++ internal/middleware/auth.go | 8 ++-- internal/oauth/routes.go | 13 +++--- internal/user/routes.go | 18 ++++++-- internal/{auth => util}/jwt.go | 2 +- queries/api_services.sql | 10 ++--- web/src/api/admin/apiServices.ts | 8 ++++ 10 files changed, 133 insertions(+), 59 deletions(-) rename internal/{apiservices => admin}/routes.go (68%) create mode 100644 internal/middleware/admin.go rename internal/{auth => util}/jwt.go (99%) create mode 100644 web/src/api/admin/apiServices.ts diff --git a/cmd/hspguard/api/api.go b/cmd/hspguard/api/api.go index ad47482..f4a501d 100644 --- a/cmd/hspguard/api/api.go +++ b/cmd/hspguard/api/api.go @@ -6,7 +6,7 @@ import ( "net/http" "os" - "gitea.local/admin/hspguard/internal/apiservices" + "gitea.local/admin/hspguard/internal/admin" "gitea.local/admin/hspguard/internal/auth" "gitea.local/admin/hspguard/internal/config" imiddleware "gitea.local/admin/hspguard/internal/middleware" @@ -45,17 +45,15 @@ func (s *APIServer) Run() error { oauthHandler := oauth.NewOAuthHandler(s.repo, s.cfg) router.Route("/api/v1", func(r chi.Router) { - am := imiddleware.New(s.cfg) + authMiddleware := imiddleware.NewAuthMiddleware(s.cfg) r.Use(imiddleware.WithSkipper( - am.Runner, - "/api/v1/auth/login", + authMiddleware.Runner, "/api/v1/register", - "/api/v1/auth/refresh", "/api/v1/oauth/token", "/api/v1/avatar", )) - userHandler := user.NewUserHandler(s.repo, s.storage) + userHandler := user.NewUserHandler(s.repo, s.storage, s.cfg) userHandler.RegisterRoutes(r) authHandler := auth.NewAuthHandler(s.repo, s.cfg) @@ -63,8 +61,8 @@ func (s *APIServer) Run() error { oauthHandler.RegisterRoutes(r) - apiServicesHandler := apiservices.New(s.repo, s.cfg) - apiServicesHandler.RegisterRoutes(r) + adminHandler := admin.New(s.repo, s.cfg) + adminHandler.RegisterRoutes(r) }) router.Get("/.well-known/jwks.json", oauthHandler.WriteJWKS) diff --git a/internal/apiservices/routes.go b/internal/admin/routes.go similarity index 68% rename from internal/apiservices/routes.go rename to internal/admin/routes.go index e76d2e2..0f2ec8c 100644 --- a/internal/apiservices/routes.go +++ b/internal/admin/routes.go @@ -1,31 +1,49 @@ -package apiservices +package admin import ( "encoding/json" + "log" "net/http" "gitea.local/admin/hspguard/internal/config" + imiddleware "gitea.local/admin/hspguard/internal/middleware" "gitea.local/admin/hspguard/internal/repository" "gitea.local/admin/hspguard/internal/util" "gitea.local/admin/hspguard/internal/web" "github.com/go-chi/chi/v5" - "github.com/google/uuid" ) -type ApiServicesHandler struct { +type AdminHandler struct { repo *repository.Queries cfg *config.AppConfig } -func New(repo *repository.Queries, cfg *config.AppConfig) *ApiServicesHandler { - return &ApiServicesHandler{ +func New(repo *repository.Queries, cfg *config.AppConfig) *AdminHandler { + return &AdminHandler{ repo, cfg, } } -func (h *ApiServicesHandler) RegisterRoutes(router chi.Router) { - router.Post("/api-services/create", h.Add) +func (h *AdminHandler) RegisterRoutes(router chi.Router) { + router.Route("/admin", func(r chi.Router) { + authMiddleware := imiddleware.NewAuthMiddleware(h.cfg) + adminMiddleware := imiddleware.NewAdminMiddleware(h.repo) + r.Use(authMiddleware.Runner, adminMiddleware.Runner) + + r.Post("/api-services/create", h.AddApiService) + }) +} + +func (h *AdminHandler) GetApiServices(w http.ResponseWriter, r *http.Request) { + services, err := h.repo.ListApiServices(r.Context()) + if err != nil { + log.Println("ERR: Failed to list api services from db:", err) + web.Error(w, "failed to get api services", http.StatusInternalServerError) + return + } + + } type AddServiceRequest struct { @@ -35,24 +53,7 @@ type AddServiceRequest struct { GrantTypes []string `json:"grant_types"` } -func (h *ApiServicesHandler) Add(w http.ResponseWriter, r *http.Request) { - userId, ok := util.GetRequestUserId(r.Context()) - if !ok { - web.Error(w, "failed to get user id from auth session", http.StatusInternalServerError) - return - } - - user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId)) - if err != nil { - web.Error(w, "user with provided id does not exist", http.StatusUnauthorized) - return - } - - if !user.IsAdmin { - web.Error(w, "you cannot create api services", http.StatusForbidden) - return - } - +func (h *AdminHandler) AddApiService(w http.ResponseWriter, r *http.Request) { var req AddServiceRequest decoder := json.NewDecoder(r.Body) diff --git a/internal/auth/routes.go b/internal/auth/routes.go index 124ad6a..085ec2b 100644 --- a/internal/auth/routes.go +++ b/internal/auth/routes.go @@ -8,6 +8,7 @@ import ( "time" "gitea.local/admin/hspguard/internal/config" + imiddleware "gitea.local/admin/hspguard/internal/middleware" "gitea.local/admin/hspguard/internal/repository" "gitea.local/admin/hspguard/internal/types" "gitea.local/admin/hspguard/internal/util" @@ -34,7 +35,7 @@ func (h *AuthHandler) signTokens(user *repository.User) (string, string, error) }, } - accessToken, err := SignJwtToken(accessClaims, h.cfg.Jwt.PrivateKey) + accessToken, err := util.SignJwtToken(accessClaims, h.cfg.Jwt.PrivateKey) if err != nil { return "", "", err } @@ -50,7 +51,7 @@ func (h *AuthHandler) signTokens(user *repository.User) (string, string, error) }, } - refreshToken, err := SignJwtToken(refreshClaims, h.cfg.Jwt.PrivateKey) + refreshToken, err := util.SignJwtToken(refreshClaims, h.cfg.Jwt.PrivateKey) if err != nil { return "", "", err } @@ -66,9 +67,17 @@ func NewAuthHandler(repo *repository.Queries, cfg *config.AppConfig) *AuthHandle } func (h *AuthHandler) RegisterRoutes(api chi.Router) { - api.Get("/auth/profile", h.getProfile) - api.Post("/auth/login", h.login) - api.Post("/auth/refresh", h.refreshToken) + api.Route("/auth", func(r chi.Router) { + r.Group(func(protected chi.Router) { + authMiddleware := imiddleware.NewAuthMiddleware(h.cfg) + protected.Use(authMiddleware.Runner) + + protected.Get("/profile", h.getProfile) + }) + + r.Post("/login", h.login) + r.Post("/refresh", h.refreshToken) + }) } func (h *AuthHandler) refreshToken(w http.ResponseWriter, r *http.Request) { @@ -85,7 +94,7 @@ func (h *AuthHandler) refreshToken(w http.ResponseWriter, r *http.Request) { } tokenStr := parts[1] - token, userClaims, err := VerifyToken(tokenStr, h.cfg.Jwt.PublicKey) + token, userClaims, err := util.VerifyToken(tokenStr, h.cfg.Jwt.PublicKey) if err != nil || !token.Valid { http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized) return diff --git a/internal/middleware/admin.go b/internal/middleware/admin.go new file mode 100644 index 0000000..0146d8d --- /dev/null +++ b/internal/middleware/admin.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "log" + "net/http" + + "gitea.local/admin/hspguard/internal/repository" + "gitea.local/admin/hspguard/internal/util" + "gitea.local/admin/hspguard/internal/web" + "github.com/google/uuid" +) + +type AdminMiddleware struct { + repo *repository.Queries +} + +func NewAdminMiddleware(repo *repository.Queries) *AdminMiddleware { + return &AdminMiddleware{ + repo, + } +} + +func (m *AdminMiddleware) Runner(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userId, ok := util.GetRequestUserId(r.Context()) + if !ok { + log.Println("ERR: Could not get user id from request") + web.Error(w, "not authenticated", http.StatusUnauthorized) + return + } + + user, err := m.repo.FindUserId(r.Context(), uuid.MustParse(userId)) + if err != nil { + log.Println("ERR: User with provided id does not exist:", userId) + web.Error(w, "not authenticated", http.StatusUnauthorized) + return + } + + if !user.IsAdmin { + log.Println("INFO: User is not admin") + web.Error(w, "no priviligies to access this resource", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 513631c..7000a66 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -6,9 +6,9 @@ import ( "net/http" "strings" - "gitea.local/admin/hspguard/internal/auth" "gitea.local/admin/hspguard/internal/config" "gitea.local/admin/hspguard/internal/types" + "gitea.local/admin/hspguard/internal/util" "gitea.local/admin/hspguard/internal/web" ) @@ -16,7 +16,7 @@ type AuthMiddleware struct { cfg *config.AppConfig } -func New(cfg *config.AppConfig) *AuthMiddleware { +func NewAuthMiddleware(cfg *config.AppConfig) *AuthMiddleware { return &AuthMiddleware{ cfg, } @@ -37,9 +37,9 @@ func (m *AuthMiddleware) Runner(next http.Handler) http.Handler { } tokenStr := parts[1] - token, userClaims, err := auth.VerifyToken(tokenStr, m.cfg.Jwt.PublicKey) + token, userClaims, err := util.VerifyToken(tokenStr, m.cfg.Jwt.PublicKey) if err != nil || !token.Valid { - http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized) + web.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized) return } diff --git a/internal/oauth/routes.go b/internal/oauth/routes.go index ff821c8..97a44f1 100644 --- a/internal/oauth/routes.go +++ b/internal/oauth/routes.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "gitea.local/admin/hspguard/internal/auth" "gitea.local/admin/hspguard/internal/config" "gitea.local/admin/hspguard/internal/repository" "gitea.local/admin/hspguard/internal/types" @@ -32,14 +31,16 @@ func NewOAuthHandler(repo *repository.Queries, cfg *config.AppConfig) *OAuthHand } } -func (h *OAuthHandler) RegisterRoutes(r chi.Router) { - r.Post("/oauth/token", h.tokenEndpoint) +func (h *OAuthHandler) RegisterRoutes(router chi.Router) { + router.Route("/oauth", func(r chi.Router) { + r.Post("/token", h.tokenEndpoint) - r.Post("/oauth/code", h.getAuthCode) + r.Post("/code", h.getAuthCode) + }) } func (h *OAuthHandler) WriteJWKS(w http.ResponseWriter, r *http.Request) { - pubKey, err := auth.ParseBase64PublicKey(h.cfg.Jwt.PublicKey) + pubKey, err := util.ParseBase64PublicKey(h.cfg.Jwt.PublicKey) if err != nil { web.Error(w, "failed to parse public key", http.StatusInternalServerError) } @@ -207,7 +208,7 @@ func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) { }, } - idToken, err := auth.SignJwtToken(claims, h.cfg.Jwt.PrivateKey) + idToken, err := util.SignJwtToken(claims, h.cfg.Jwt.PrivateKey) if err != nil { web.Error(w, "failed to sign id token", http.StatusInternalServerError) return diff --git a/internal/user/routes.go b/internal/user/routes.go index 72b3044..e4685d3 100644 --- a/internal/user/routes.go +++ b/internal/user/routes.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "gitea.local/admin/hspguard/internal/config" + imiddleware "gitea.local/admin/hspguard/internal/middleware" "gitea.local/admin/hspguard/internal/repository" "gitea.local/admin/hspguard/internal/storage" "gitea.local/admin/hspguard/internal/util" @@ -24,18 +26,26 @@ import ( type UserHandler struct { repo *repository.Queries minio *storage.FileStorage + cfg *config.AppConfig } -func NewUserHandler(repo *repository.Queries, minio *storage.FileStorage) *UserHandler { +func NewUserHandler(repo *repository.Queries, minio *storage.FileStorage, cfg *config.AppConfig) *UserHandler { return &UserHandler{ - repo: repo, - minio: minio, + repo, + minio, + cfg, } } func (h *UserHandler) RegisterRoutes(api chi.Router) { + api.Group(func(protected chi.Router) { + authMiddleware := imiddleware.NewAuthMiddleware(h.cfg) + protected.Use(authMiddleware.Runner) + + protected.Put("/avatar", h.uploadAvatar) + }) + api.Post("/register", h.register) - api.Put("/avatar", h.uploadAvatar) api.Get("/avatar/{avatar}", h.getAvatar) } diff --git a/internal/auth/jwt.go b/internal/util/jwt.go similarity index 99% rename from internal/auth/jwt.go rename to internal/util/jwt.go index fc1a5bf..e0861de 100644 --- a/internal/auth/jwt.go +++ b/internal/util/jwt.go @@ -1,4 +1,4 @@ -package auth +package util import ( "crypto/rsa" diff --git a/queries/api_services.sql b/queries/api_services.sql index 340a68d..97790cf 100644 --- a/queries/api_services.sql +++ b/queries/api_services.sql @@ -3,16 +3,16 @@ INSERT INTO api_services ( client_id, client_secret, name, redirect_uris, scopes, grant_types ) VALUES ( $1, $2, $3, $4, $5, $6 -) RETURNING *; +) RETURNING id,client_id,name,redirect_uris,scopes,grant_types,created_at,updated_at,is_active; -- name: GetApiServiceCID :one -SELECT * FROM api_services +SELECT id,client_id,name,redirect_uris,scopes,grant_types,created_at,updated_at,is_active FROM api_services WHERE client_id = $1 AND is_active = true LIMIT 1; -- name: ListApiServices :many -SELECT * FROM api_services +SELECT id,client_id,name,redirect_uris,scopes,grant_types,created_at,updated_at,is_active FROM api_services ORDER BY created_at DESC; -- name: UpdateApiService :one @@ -24,7 +24,7 @@ SET grant_types = $5, updated_at = NOW() WHERE client_id = $1 -RETURNING *; +RETURNING id,client_id,name,redirect_uris,scopes,grant_types,created_at,updated_at,is_active; -- name: DeactivateApiService :exec UPDATE api_services @@ -36,4 +36,4 @@ WHERE client_id = $1; UPDATE api_services SET client_secret = $2, updated_at = NOW() -WHERE client_id = $1; \ No newline at end of file +WHERE client_id = $1; diff --git a/web/src/api/admin/apiServices.ts b/web/src/api/admin/apiServices.ts new file mode 100644 index 0000000..76643b9 --- /dev/null +++ b/web/src/api/admin/apiServices.ts @@ -0,0 +1,8 @@ +import { axios } from ".." + + +export interface FetchApiServicesRequest {} + +export const fetchApiServices = async (req: FetchApiServicesRequest) => { + const response = await axios.get() +}