Compare commits
19 Commits
db2cb36f54
...
800e1afbe5
Author | SHA1 | Date | |
---|---|---|---|
800e1afbe5 | |||
8abc4396ac | |||
70f860824c | |||
3c5e31cbb2 | |||
3923b428a4 | |||
5b816c6873 | |||
66edadfeda | |||
b872722e07 | |||
091218b42d | |||
03697b2f67 | |||
8a28fca3d9 | |||
1ab4113040 | |||
013f300513 | |||
45e31b41ca | |||
182f30f1ba | |||
7c97ebd84f | |||
9fefe3ac71 | |||
ca3006c428 | |||
51b7e6b3f9 |
63
.air.toml
Normal file
63
.air.toml
Normal file
@ -0,0 +1,63 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "bin\\hspguard"
|
||||
cmd = "make build"
|
||||
delay = 1000
|
||||
exclude_dir = [
|
||||
"assets",
|
||||
"tmp",
|
||||
"vendor",
|
||||
"testdata",
|
||||
"dist",
|
||||
"migrations",
|
||||
"queries",
|
||||
"scripts",
|
||||
"templates",
|
||||
"web",
|
||||
]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
silent = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[proxy]
|
||||
app_port = 0
|
||||
enabled = false
|
||||
proxy_port = 0
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,6 +13,8 @@
|
||||
|
||||
bin/*
|
||||
|
||||
tmp/
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
|
19
Makefile
19
Makefile
@ -1,11 +1,14 @@
|
||||
|
||||
# Project metadata
|
||||
APP_NAME := hspguard
|
||||
CMD_DIR := ./cmd/$(APP_NAME)
|
||||
BIN_DIR := ./bin
|
||||
BIN_PATH := $(BIN_DIR)/$(APP_NAME)
|
||||
|
||||
# Detect platform and add .exe suffix on Windows
|
||||
OS := $(shell go env GOOS)
|
||||
EXT := $(if $(filter windows,$(OS)),.exe,)
|
||||
BIN_PATH := $(BIN_DIR)/$(APP_NAME)$(EXT)
|
||||
|
||||
PKG := ./...
|
||||
GO_FILES := $(shell find . -type f -name '*.go' -not -path "./vendor/*")
|
||||
|
||||
# Go tools
|
||||
GO := go
|
||||
@ -16,21 +19,20 @@ GOTEST := go test
|
||||
# Build flags
|
||||
LD_FLAGS := -s -w
|
||||
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>NUL || echo unknown)
|
||||
|
||||
.PHONY: all build clean fmt lint run
|
||||
.PHONY: all build clean fmt lint run test mod
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
@mkdir -p $(BIN_DIR)
|
||||
$(GO) build -ldflags "-X main.buildTime=$(BUILD_TIME) -X main.commitHash=$(GIT_COMMIT) $(LD_FLAGS)" -o $(BIN_PATH) $(CMD_DIR)
|
||||
|
||||
run:
|
||||
$(GO) run $(CMD_DIR)
|
||||
|
||||
fmt:
|
||||
$(GOFMT) -s -w $(GO_FILES)
|
||||
$(GOFMT) -s -w .
|
||||
|
||||
lint:
|
||||
$(GOLINT) run
|
||||
@ -39,8 +41,7 @@ test:
|
||||
$(GOTEST) -v $(PKG)
|
||||
|
||||
clean:
|
||||
@rm -rf $(BIN_DIR)
|
||||
@if [ -d "$(BIN_DIR)" ]; then rm -rf $(BIN_DIR); fi
|
||||
|
||||
mod:
|
||||
$(GO) mod tidy
|
||||
|
||||
|
@ -6,10 +6,9 @@ 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"
|
||||
"gitea.local/admin/hspguard/internal/oauth"
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"gitea.local/admin/hspguard/internal/storage"
|
||||
@ -45,17 +44,7 @@ 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)
|
||||
r.Use(imiddleware.WithSkipper(
|
||||
am.Runner,
|
||||
"/api/v1/auth/login",
|
||||
"/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 +52,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)
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"gitea.local/admin/hspguard/internal/storage"
|
||||
"gitea.local/admin/hspguard/internal/user"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
@ -31,7 +31,7 @@ func main() {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
conn, err := pgx.Connect(ctx, cfg.DatabaseURL)
|
||||
conn, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
log.Fatalln("ERR: Failed to connect to db:", err)
|
||||
return
|
||||
|
2
go.mod
2
go.mod
@ -16,6 +16,7 @@ require (
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/minio/crc64nvme v1.0.1 // indirect
|
||||
@ -26,6 +27,7 @@ require (
|
||||
github.com/tinylib/msgp v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
)
|
||||
|
34
internal/admin/apiservices.go
Normal file
34
internal/admin/apiservices.go
Normal file
@ -0,0 +1,34 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ApiServiceDTO struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ClientID string `json:"client_id"`
|
||||
Name string `json:"name"`
|
||||
RedirectUris []string `json:"redirect_uris"`
|
||||
Scopes []string `json:"scopes"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func NewApiServiceDTO(service repository.ApiService) ApiServiceDTO {
|
||||
return ApiServiceDTO{
|
||||
ID: service.ID,
|
||||
ClientID: service.ClientID,
|
||||
Name: service.Name,
|
||||
RedirectUris: service.RedirectUris,
|
||||
Scopes: service.Scopes,
|
||||
GrantTypes: service.GrantTypes,
|
||||
CreatedAt: service.CreatedAt,
|
||||
UpdatedAt: service.UpdatedAt,
|
||||
IsActive: false,
|
||||
}
|
||||
}
|
@ -1,31 +1,68 @@
|
||||
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.Get("/api-services", h.GetApiServices)
|
||||
r.Post("/api-services", 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
|
||||
}
|
||||
|
||||
apiServices := make([]ApiServiceDTO, 0)
|
||||
|
||||
for _, service := range services {
|
||||
apiServices = append(apiServices, NewApiServiceDTO(service))
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Items []ApiServiceDTO `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
|
||||
if err := encoder.Encode(Response{
|
||||
Items: apiServices,
|
||||
Count: len(apiServices),
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
type AddServiceRequest struct {
|
||||
@ -35,24 +72,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)
|
||||
@ -103,7 +123,7 @@ func (h *ApiServicesHandler) Add(w http.ResponseWriter, r *http.Request) {
|
||||
service.ClientSecret = clientSecret
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
if err := encoder.Encode(service); err != nil {
|
||||
if err := encoder.Encode(NewApiServiceDTO(service)); err != nil {
|
||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
@ -3,11 +3,13 @@ package auth
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"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/types"
|
||||
"gitea.local/admin/hspguard/internal/util"
|
||||
@ -30,11 +32,11 @@ func (h *AuthHandler) signTokens(user *repository.User) (string, string, error)
|
||||
Issuer: h.cfg.Jwt.Issuer,
|
||||
Subject: user.ID.String(),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Second)),
|
||||
},
|
||||
}
|
||||
|
||||
accessToken, err := SignJwtToken(accessClaims, h.cfg.Jwt.PrivateKey)
|
||||
accessToken, err := util.SignJwtToken(accessClaims, h.cfg.Jwt.PrivateKey)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@ -50,7 +52,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 +68,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 +95,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
|
||||
@ -184,6 +194,8 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: looking for user with following params: %#v\n", params)
|
||||
|
||||
user, err := h.repo.FindUserEmail(r.Context(), params.Email)
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided email does not exists", http.StatusBadRequest)
|
||||
|
47
internal/middleware/admin.go
Normal file
47
internal/middleware/admin.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package auth
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
@ -36,4 +36,4 @@ WHERE client_id = $1;
|
||||
UPDATE api_services
|
||||
SET client_secret = $2,
|
||||
updated_at = NOW()
|
||||
WHERE client_id = $1;
|
||||
WHERE client_id = $1;
|
||||
|
6
web/globals.d.ts
vendored
Normal file
6
web/globals.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
interface Window {
|
||||
guard: {
|
||||
refreshing: boolean;
|
||||
refreshQueue: ((token: string | null) => void)[];
|
||||
};
|
||||
}
|
18
web/src/api/admin/apiServices.ts
Normal file
18
web/src/api/admin/apiServices.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { ApiService } from "@/types";
|
||||
import { axios, handleApiError } from "..";
|
||||
|
||||
export interface FetchApiServicesResponse {
|
||||
items: ApiService[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const getApiServices = async (): Promise<FetchApiServicesResponse> => {
|
||||
const response = await axios.get<FetchApiServicesResponse>(
|
||||
"/api/v1/admin/api-services",
|
||||
);
|
||||
|
||||
if (response.status !== 200 && response.status !== 201)
|
||||
throw await handleApiError(response);
|
||||
|
||||
return response.data;
|
||||
};
|
@ -12,7 +12,6 @@ export const axios = Axios.create({
|
||||
},
|
||||
});
|
||||
|
||||
let isRefreshing = false;
|
||||
let refreshQueue: ((token: string | null) => void)[] = [];
|
||||
|
||||
const waitForTokenRefresh = () => {
|
||||
@ -60,7 +59,7 @@ const refreshToken = async (
|
||||
} finally {
|
||||
localStorage.removeItem("refreshing");
|
||||
loadAccounts?.();
|
||||
isRefreshing = false;
|
||||
window.guard.refreshing = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -74,8 +73,9 @@ axios.interceptors.request.use(
|
||||
return request;
|
||||
}
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
if (!window.guard.refreshing) {
|
||||
console.log(`request to ${request.url} is refreshing token`);
|
||||
window.guard.refreshing = true;
|
||||
try {
|
||||
const { access } = await refreshToken(
|
||||
account!.accountId,
|
||||
@ -87,7 +87,9 @@ axios.interceptors.request.use(
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
console.log(`request to ${request.url} is waiting for token`);
|
||||
token = await waitForTokenRefresh();
|
||||
console.log(`request to ${request.url} waited for token:`, token);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
|
@ -1,12 +1,12 @@
|
||||
import type { FC } from "react";
|
||||
import { useBarItems } from "../tabs";
|
||||
import { useBarItems } from "@/hooks/barItems";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const Sidebar: FC = () => {
|
||||
const [barItems, isActive] = useBarItems();
|
||||
|
||||
return (
|
||||
<div className="hidden sm:flex flex-col gap-2 items-stretch min-w-80 w-80 p-5 pt-18 min-h-screen select-none bg-white/65 dark:bg-black/65 shadow-lg shadow-gray-300 dark:shadow-gray-700">
|
||||
<div className="hidden sm:flex flex-col gap-2 items-stretch border-r border-gray-700 dark:border-gray-800 min-w-80 w-80 p-5 pt-18 min-h-screen select-none">
|
||||
{barItems.map((item) => (
|
||||
<Link to={item.pathname} key={item.tab}>
|
||||
<div
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { type FC } from "react";
|
||||
import { useBarItems } from "../tabs";
|
||||
import { useBarItems } from "@/hooks/barItems";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const TopBar: FC = () => {
|
||||
|
@ -19,16 +19,17 @@ const DashboardLayout: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="w-full space-y-4 flex-1" spacing={false}>
|
||||
<CardContent
|
||||
className="w-full space-y-4 flex-1 bg-black/5 dark:bg-white/5"
|
||||
spacing={false}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<Sidebar />
|
||||
<div className="sm:p-4 max-w-full flex-1">
|
||||
<div className="max-w-full flex-1">
|
||||
<div className="flex flex-col w-full items-center gap-2">
|
||||
<TopBar />
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
@ -4,6 +4,13 @@ import App from "./App";
|
||||
import "./index.css";
|
||||
import { OAuthProvider } from "./context/oauth/provider";
|
||||
|
||||
if (typeof window.guard !== "object") {
|
||||
window.guard = {
|
||||
refreshing: false,
|
||||
refreshQueue: [],
|
||||
};
|
||||
}
|
||||
|
||||
const root = document.getElementById("root")!;
|
||||
|
||||
createRoot(root).render(
|
||||
|
@ -1,100 +1,132 @@
|
||||
import type { FC } from "react";
|
||||
import { useAdmin } from "@/store/admin";
|
||||
import { useEffect, type FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const services = [
|
||||
{
|
||||
id: "1",
|
||||
name: "User Service",
|
||||
clientId: "user-svc-001",
|
||||
isActive: true,
|
||||
createdAt: "2024-09-15T10:20:30Z",
|
||||
updatedAt: "2025-01-10T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Billing Service",
|
||||
clientId: "billing-svc-009",
|
||||
isActive: false,
|
||||
createdAt: "2024-10-01T08:45:10Z",
|
||||
updatedAt: "2025-03-22T14:30:00Z",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Analytics Service",
|
||||
clientId: "analytics-svc-777",
|
||||
isActive: true,
|
||||
createdAt: "2024-11-25T16:00:00Z",
|
||||
updatedAt: "2025-02-05T10:15:45Z",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Email Service",
|
||||
clientId: "email-svc-333",
|
||||
isActive: false,
|
||||
createdAt: "2023-07-10T13:00:00Z",
|
||||
updatedAt: "2024-12-31T09:25:00Z",
|
||||
},
|
||||
];
|
||||
// const services = [
|
||||
// {
|
||||
// id: "1",
|
||||
// name: "User Service",
|
||||
// clientId: "user-svc-001",
|
||||
// isActive: true,
|
||||
// createdAt: "2024-09-15T10:20:30Z",
|
||||
// updatedAt: "2025-01-10T12:00:00Z",
|
||||
// },
|
||||
// {
|
||||
// id: "2",
|
||||
// name: "Billing Service",
|
||||
// clientId: "billing-svc-009",
|
||||
// isActive: false,
|
||||
// createdAt: "2024-10-01T08:45:10Z",
|
||||
// updatedAt: "2025-03-22T14:30:00Z",
|
||||
// },
|
||||
// {
|
||||
// id: "3",
|
||||
// name: "Analytics Service",
|
||||
// clientId: "analytics-svc-777",
|
||||
// isActive: true,
|
||||
// createdAt: "2024-11-25T16:00:00Z",
|
||||
// updatedAt: "2025-02-05T10:15:45Z",
|
||||
// },
|
||||
// {
|
||||
// id: "4",
|
||||
// name: "Email Service",
|
||||
// clientId: "email-svc-333",
|
||||
// isActive: false,
|
||||
// createdAt: "2023-07-10T13:00:00Z",
|
||||
// updatedAt: "2024-12-31T09:25:00Z",
|
||||
// },
|
||||
// ];
|
||||
|
||||
const ApiServicesPage: FC = () => {
|
||||
const apiServices = useAdmin((state) => state.apiServices);
|
||||
const loading = useAdmin((state) => state.loadingApiServices);
|
||||
const fetchApiServices = useAdmin((state) => state.fetchApiServices);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApiServices();
|
||||
}, [fetchApiServices]);
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded shadow-md dark:shadow-gray-800">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<div className="relative overflow-x-auto flex flex-col w-full h-full">
|
||||
<div className="p-4">
|
||||
<p className="text-gray-800 dark:text-gray-300">Search...</p>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/60 dark:bg-gray-900/60 backdrop-blur-sm">
|
||||
<div className="text-gray-800 dark:text-gray-200 font-medium">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table className="min-w-full flex-1 border-l-0 border border-gray-700 dark:border-gray-800 border-collapse divide-y divide-gray-200 dark:divide-gray-800">
|
||||
<thead className="bg-black/5 dark:bg-white/5">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-white/70 border border-l-0 border-gray-700 dark:border-gray-800">
|
||||
Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-white/70 border border-l-0 border-gray-700 dark:border-gray-800">
|
||||
Client ID
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-white/70 border border-l-0 border-gray-700 dark:border-gray-800">
|
||||
Is Active
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-white/70 border border-l-0 border-gray-700 dark:border-gray-800">
|
||||
Created At
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-200">
|
||||
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-700 dark:text-white/70 border border-l-0 border-gray-700 dark:border-gray-800">
|
||||
Updated At
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{services.map((service) => (
|
||||
<tr
|
||||
key={service.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<td className="px-6 py-4 text-sm font-medium text-blue-600">
|
||||
<Link
|
||||
to={`/services/${service.id}`}
|
||||
className="hover:underline hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
{service.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{service.clientId}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
<span
|
||||
className={`inline-block px-2 py-1 text-xs rounded-full font-semibold ${
|
||||
service.isActive
|
||||
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
|
||||
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
||||
}`}
|
||||
>
|
||||
{service.isActive ? "Yes" : "No"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(service.createdAt).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(service.updatedAt).toLocaleString()}
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{!loading && apiServices.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={5}
|
||||
className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
No services found.
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
) : (
|
||||
apiServices.map((service) => (
|
||||
<tr
|
||||
key={service.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<td className="px-6 py-4 text-sm font-medium text-blue-600 border border-gray-700">
|
||||
<Link
|
||||
to={`/services/${service.id}`}
|
||||
className="hover:underline hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
{service.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300 border border-gray-700">
|
||||
{service.client_id}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm border border-gray-700">
|
||||
<span
|
||||
className={`inline-block px-2 py-1 text-xs rounded-full font-semibold ${
|
||||
service.is_active
|
||||
? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
|
||||
: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
|
||||
}`}
|
||||
>
|
||||
{service.is_active ? "Yes" : "No"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 border border-gray-700">
|
||||
{new Date(service.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 border border-gray-700">
|
||||
{new Date(service.updated_at).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@ const PersonalInfoPage: FC = () => {
|
||||
const profile = useAuth((state) => state.profile);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-4 sm:p-8">
|
||||
<h1 className="dark:text-gray-200 text-gray-800 text-2xl">
|
||||
Your profile info in Home services
|
||||
</h1>
|
||||
@ -61,7 +61,7 @@ const PersonalInfoPage: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
28
web/src/store/admin.ts
Normal file
28
web/src/store/admin.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { getApiServices } from "@/api/admin/apiServices";
|
||||
import type { ApiService } from "@/types";
|
||||
import { create } from "zustand";
|
||||
|
||||
interface IAdminState {
|
||||
apiServices: ApiService[];
|
||||
loadingApiServices: boolean;
|
||||
|
||||
fetchApiServices: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useAdmin = create<IAdminState>((set) => ({
|
||||
apiServices: [],
|
||||
loadingApiServices: false,
|
||||
|
||||
fetchApiServices: async () => {
|
||||
set({ loadingApiServices: true });
|
||||
|
||||
try {
|
||||
const response = await getApiServices();
|
||||
set({ apiServices: response.items });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to fetch services:", err);
|
||||
} finally {
|
||||
set({ loadingApiServices: false });
|
||||
}
|
||||
},
|
||||
}));
|
@ -9,3 +9,15 @@ export interface UserProfile {
|
||||
updated_at: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ApiService {
|
||||
id: string;
|
||||
client_id: string;
|
||||
name: string;
|
||||
redirect_uris: string[];
|
||||
scopes: string[];
|
||||
grant_types: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
@ -25,5 +25,5 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "globals.d.ts"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user