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/*
|
bin/*
|
||||||
|
|
||||||
|
tmp/
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
19
Makefile
19
Makefile
@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
# Project metadata
|
# Project metadata
|
||||||
APP_NAME := hspguard
|
APP_NAME := hspguard
|
||||||
CMD_DIR := ./cmd/$(APP_NAME)
|
CMD_DIR := ./cmd/$(APP_NAME)
|
||||||
BIN_DIR := ./bin
|
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 := ./...
|
PKG := ./...
|
||||||
GO_FILES := $(shell find . -type f -name '*.go' -not -path "./vendor/*")
|
|
||||||
|
|
||||||
# Go tools
|
# Go tools
|
||||||
GO := go
|
GO := go
|
||||||
@ -16,21 +19,20 @@ GOTEST := go test
|
|||||||
# Build flags
|
# Build flags
|
||||||
LD_FLAGS := -s -w
|
LD_FLAGS := -s -w
|
||||||
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
|
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
|
all: build
|
||||||
|
|
||||||
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)
|
$(GO) build -ldflags "-X main.buildTime=$(BUILD_TIME) -X main.commitHash=$(GIT_COMMIT) $(LD_FLAGS)" -o $(BIN_PATH) $(CMD_DIR)
|
||||||
|
|
||||||
run:
|
run:
|
||||||
$(GO) run $(CMD_DIR)
|
$(GO) run $(CMD_DIR)
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
$(GOFMT) -s -w $(GO_FILES)
|
$(GOFMT) -s -w .
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
$(GOLINT) run
|
$(GOLINT) run
|
||||||
@ -39,8 +41,7 @@ test:
|
|||||||
$(GOTEST) -v $(PKG)
|
$(GOTEST) -v $(PKG)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -rf $(BIN_DIR)
|
@if [ -d "$(BIN_DIR)" ]; then rm -rf $(BIN_DIR); fi
|
||||||
|
|
||||||
mod:
|
mod:
|
||||||
$(GO) mod tidy
|
$(GO) mod tidy
|
||||||
|
|
||||||
|
@ -6,10 +6,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"gitea.local/admin/hspguard/internal/apiservices"
|
"gitea.local/admin/hspguard/internal/admin"
|
||||||
"gitea.local/admin/hspguard/internal/auth"
|
"gitea.local/admin/hspguard/internal/auth"
|
||||||
"gitea.local/admin/hspguard/internal/config"
|
"gitea.local/admin/hspguard/internal/config"
|
||||||
imiddleware "gitea.local/admin/hspguard/internal/middleware"
|
|
||||||
"gitea.local/admin/hspguard/internal/oauth"
|
"gitea.local/admin/hspguard/internal/oauth"
|
||||||
"gitea.local/admin/hspguard/internal/repository"
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
"gitea.local/admin/hspguard/internal/storage"
|
"gitea.local/admin/hspguard/internal/storage"
|
||||||
@ -45,17 +44,7 @@ func (s *APIServer) Run() error {
|
|||||||
oauthHandler := oauth.NewOAuthHandler(s.repo, s.cfg)
|
oauthHandler := oauth.NewOAuthHandler(s.repo, s.cfg)
|
||||||
|
|
||||||
router.Route("/api/v1", func(r chi.Router) {
|
router.Route("/api/v1", func(r chi.Router) {
|
||||||
am := imiddleware.New(s.cfg)
|
userHandler := user.NewUserHandler(s.repo, s.storage, 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.RegisterRoutes(r)
|
userHandler.RegisterRoutes(r)
|
||||||
|
|
||||||
authHandler := auth.NewAuthHandler(s.repo, s.cfg)
|
authHandler := auth.NewAuthHandler(s.repo, s.cfg)
|
||||||
@ -63,8 +52,8 @@ func (s *APIServer) Run() error {
|
|||||||
|
|
||||||
oauthHandler.RegisterRoutes(r)
|
oauthHandler.RegisterRoutes(r)
|
||||||
|
|
||||||
apiServicesHandler := apiservices.New(s.repo, s.cfg)
|
adminHandler := admin.New(s.repo, s.cfg)
|
||||||
apiServicesHandler.RegisterRoutes(r)
|
adminHandler.RegisterRoutes(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
router.Get("/.well-known/jwks.json", oauthHandler.WriteJWKS)
|
router.Get("/.well-known/jwks.json", oauthHandler.WriteJWKS)
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
"gitea.local/admin/hspguard/internal/repository"
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
"gitea.local/admin/hspguard/internal/storage"
|
"gitea.local/admin/hspguard/internal/storage"
|
||||||
"gitea.local/admin/hspguard/internal/user"
|
"gitea.local/admin/hspguard/internal/user"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ func main() {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
conn, err := pgx.Connect(ctx, cfg.DatabaseURL)
|
conn, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("ERR: Failed to connect to db:", err)
|
log.Fatalln("ERR: Failed to connect to db:", err)
|
||||||
return
|
return
|
||||||
|
2
go.mod
2
go.mod
@ -16,6 +16,7 @@ require (
|
|||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/minio/crc64nvme v1.0.1 // indirect
|
github.com/minio/crc64nvme v1.0.1 // indirect
|
||||||
@ -26,6 +27,7 @@ require (
|
|||||||
github.com/tinylib/msgp v1.3.0 // indirect
|
github.com/tinylib/msgp v1.3.0 // indirect
|
||||||
golang.org/x/crypto v0.38.0 // indirect
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
golang.org/x/net 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/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.25.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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"gitea.local/admin/hspguard/internal/config"
|
"gitea.local/admin/hspguard/internal/config"
|
||||||
|
imiddleware "gitea.local/admin/hspguard/internal/middleware"
|
||||||
"gitea.local/admin/hspguard/internal/repository"
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
"gitea.local/admin/hspguard/internal/util"
|
"gitea.local/admin/hspguard/internal/util"
|
||||||
"gitea.local/admin/hspguard/internal/web"
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApiServicesHandler struct {
|
type AdminHandler struct {
|
||||||
repo *repository.Queries
|
repo *repository.Queries
|
||||||
cfg *config.AppConfig
|
cfg *config.AppConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(repo *repository.Queries, cfg *config.AppConfig) *ApiServicesHandler {
|
func New(repo *repository.Queries, cfg *config.AppConfig) *AdminHandler {
|
||||||
return &ApiServicesHandler{
|
return &AdminHandler{
|
||||||
repo,
|
repo,
|
||||||
cfg,
|
cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ApiServicesHandler) RegisterRoutes(router chi.Router) {
|
func (h *AdminHandler) RegisterRoutes(router chi.Router) {
|
||||||
router.Post("/api-services/create", h.Add)
|
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 {
|
type AddServiceRequest struct {
|
||||||
@ -35,24 +72,7 @@ type AddServiceRequest struct {
|
|||||||
GrantTypes []string `json:"grant_types"`
|
GrantTypes []string `json:"grant_types"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ApiServicesHandler) Add(w http.ResponseWriter, r *http.Request) {
|
func (h *AdminHandler) AddApiService(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
|
|
||||||
}
|
|
||||||
|
|
||||||
var req AddServiceRequest
|
var req AddServiceRequest
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
decoder := json.NewDecoder(r.Body)
|
||||||
@ -103,7 +123,7 @@ func (h *ApiServicesHandler) Add(w http.ResponseWriter, r *http.Request) {
|
|||||||
service.ClientSecret = clientSecret
|
service.ClientSecret = clientSecret
|
||||||
|
|
||||||
encoder := json.NewEncoder(w)
|
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)
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,11 +3,13 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.local/admin/hspguard/internal/config"
|
"gitea.local/admin/hspguard/internal/config"
|
||||||
|
imiddleware "gitea.local/admin/hspguard/internal/middleware"
|
||||||
"gitea.local/admin/hspguard/internal/repository"
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
"gitea.local/admin/hspguard/internal/types"
|
"gitea.local/admin/hspguard/internal/types"
|
||||||
"gitea.local/admin/hspguard/internal/util"
|
"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,
|
Issuer: h.cfg.Jwt.Issuer,
|
||||||
Subject: user.ID.String(),
|
Subject: user.ID.String(),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
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 {
|
if err != nil {
|
||||||
return "", "", err
|
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 {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
@ -66,9 +68,17 @@ func NewAuthHandler(repo *repository.Queries, cfg *config.AppConfig) *AuthHandle
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) RegisterRoutes(api chi.Router) {
|
func (h *AuthHandler) RegisterRoutes(api chi.Router) {
|
||||||
api.Get("/auth/profile", h.getProfile)
|
api.Route("/auth", func(r chi.Router) {
|
||||||
api.Post("/auth/login", h.login)
|
r.Group(func(protected chi.Router) {
|
||||||
api.Post("/auth/refresh", h.refreshToken)
|
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) {
|
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]
|
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 {
|
if err != nil || !token.Valid {
|
||||||
http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized)
|
http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
@ -184,6 +194,8 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("DEBUG: looking for user with following params: %#v\n", params)
|
||||||
|
|
||||||
user, err := h.repo.FindUserEmail(r.Context(), params.Email)
|
user, err := h.repo.FindUserEmail(r.Context(), params.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
web.Error(w, "user with provided email does not exists", http.StatusBadRequest)
|
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"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.local/admin/hspguard/internal/auth"
|
|
||||||
"gitea.local/admin/hspguard/internal/config"
|
"gitea.local/admin/hspguard/internal/config"
|
||||||
"gitea.local/admin/hspguard/internal/types"
|
"gitea.local/admin/hspguard/internal/types"
|
||||||
|
"gitea.local/admin/hspguard/internal/util"
|
||||||
"gitea.local/admin/hspguard/internal/web"
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ type AuthMiddleware struct {
|
|||||||
cfg *config.AppConfig
|
cfg *config.AppConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.AppConfig) *AuthMiddleware {
|
func NewAuthMiddleware(cfg *config.AppConfig) *AuthMiddleware {
|
||||||
return &AuthMiddleware{
|
return &AuthMiddleware{
|
||||||
cfg,
|
cfg,
|
||||||
}
|
}
|
||||||
@ -37,9 +37,9 @@ func (m *AuthMiddleware) Runner(next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tokenStr := parts[1]
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.local/admin/hspguard/internal/auth"
|
|
||||||
"gitea.local/admin/hspguard/internal/config"
|
"gitea.local/admin/hspguard/internal/config"
|
||||||
"gitea.local/admin/hspguard/internal/repository"
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
"gitea.local/admin/hspguard/internal/types"
|
"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) {
|
func (h *OAuthHandler) RegisterRoutes(router chi.Router) {
|
||||||
r.Post("/oauth/token", h.tokenEndpoint)
|
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) {
|
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 {
|
if err != nil {
|
||||||
web.Error(w, "failed to parse public key", http.StatusInternalServerError)
|
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 {
|
if err != nil {
|
||||||
web.Error(w, "failed to sign id token", http.StatusInternalServerError)
|
web.Error(w, "failed to sign id token", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -11,6 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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/repository"
|
||||||
"gitea.local/admin/hspguard/internal/storage"
|
"gitea.local/admin/hspguard/internal/storage"
|
||||||
"gitea.local/admin/hspguard/internal/util"
|
"gitea.local/admin/hspguard/internal/util"
|
||||||
@ -24,18 +26,26 @@ import (
|
|||||||
type UserHandler struct {
|
type UserHandler struct {
|
||||||
repo *repository.Queries
|
repo *repository.Queries
|
||||||
minio *storage.FileStorage
|
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{
|
return &UserHandler{
|
||||||
repo: repo,
|
repo,
|
||||||
minio: minio,
|
minio,
|
||||||
|
cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) RegisterRoutes(api chi.Router) {
|
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.Post("/register", h.register)
|
||||||
api.Put("/avatar", h.uploadAvatar)
|
|
||||||
api.Get("/avatar/{avatar}", h.getAvatar)
|
api.Get("/avatar/{avatar}", h.getAvatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package auth
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
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)[] = [];
|
let refreshQueue: ((token: string | null) => void)[] = [];
|
||||||
|
|
||||||
const waitForTokenRefresh = () => {
|
const waitForTokenRefresh = () => {
|
||||||
@ -60,7 +59,7 @@ const refreshToken = async (
|
|||||||
} finally {
|
} finally {
|
||||||
localStorage.removeItem("refreshing");
|
localStorage.removeItem("refreshing");
|
||||||
loadAccounts?.();
|
loadAccounts?.();
|
||||||
isRefreshing = false;
|
window.guard.refreshing = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -74,8 +73,9 @@ axios.interceptors.request.use(
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isRefreshing) {
|
if (!window.guard.refreshing) {
|
||||||
isRefreshing = true;
|
console.log(`request to ${request.url} is refreshing token`);
|
||||||
|
window.guard.refreshing = true;
|
||||||
try {
|
try {
|
||||||
const { access } = await refreshToken(
|
const { access } = await refreshToken(
|
||||||
account!.accountId,
|
account!.accountId,
|
||||||
@ -87,7 +87,9 @@ axios.interceptors.request.use(
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
console.log(`request to ${request.url} is waiting for token`);
|
||||||
token = await waitForTokenRefresh();
|
token = await waitForTokenRefresh();
|
||||||
|
console.log(`request to ${request.url} waited for token:`, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useBarItems } from "../tabs";
|
import { useBarItems } from "@/hooks/barItems";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
const Sidebar: FC = () => {
|
const Sidebar: FC = () => {
|
||||||
const [barItems, isActive] = useBarItems();
|
const [barItems, isActive] = useBarItems();
|
||||||
|
|
||||||
return (
|
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) => (
|
{barItems.map((item) => (
|
||||||
<Link to={item.pathname} key={item.tab}>
|
<Link to={item.pathname} key={item.tab}>
|
||||||
<div
|
<div
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import { useBarItems } from "../tabs";
|
import { useBarItems } from "@/hooks/barItems";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
const TopBar: FC = () => {
|
const TopBar: FC = () => {
|
||||||
|
@ -19,16 +19,17 @@ const DashboardLayout: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex flex-row">
|
||||||
<Sidebar />
|
<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">
|
<div className="flex flex-col w-full items-center gap-2">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<Outlet />
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
@ -4,6 +4,13 @@ import App from "./App";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { OAuthProvider } from "./context/oauth/provider";
|
import { OAuthProvider } from "./context/oauth/provider";
|
||||||
|
|
||||||
|
if (typeof window.guard !== "object") {
|
||||||
|
window.guard = {
|
||||||
|
refreshing: false,
|
||||||
|
refreshQueue: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const root = document.getElementById("root")!;
|
const root = document.getElementById("root")!;
|
||||||
|
|
||||||
createRoot(root).render(
|
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";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
const services = [
|
// const services = [
|
||||||
{
|
// {
|
||||||
id: "1",
|
// id: "1",
|
||||||
name: "User Service",
|
// name: "User Service",
|
||||||
clientId: "user-svc-001",
|
// clientId: "user-svc-001",
|
||||||
isActive: true,
|
// isActive: true,
|
||||||
createdAt: "2024-09-15T10:20:30Z",
|
// createdAt: "2024-09-15T10:20:30Z",
|
||||||
updatedAt: "2025-01-10T12:00:00Z",
|
// updatedAt: "2025-01-10T12:00:00Z",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "2",
|
// id: "2",
|
||||||
name: "Billing Service",
|
// name: "Billing Service",
|
||||||
clientId: "billing-svc-009",
|
// clientId: "billing-svc-009",
|
||||||
isActive: false,
|
// isActive: false,
|
||||||
createdAt: "2024-10-01T08:45:10Z",
|
// createdAt: "2024-10-01T08:45:10Z",
|
||||||
updatedAt: "2025-03-22T14:30:00Z",
|
// updatedAt: "2025-03-22T14:30:00Z",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "3",
|
// id: "3",
|
||||||
name: "Analytics Service",
|
// name: "Analytics Service",
|
||||||
clientId: "analytics-svc-777",
|
// clientId: "analytics-svc-777",
|
||||||
isActive: true,
|
// isActive: true,
|
||||||
createdAt: "2024-11-25T16:00:00Z",
|
// createdAt: "2024-11-25T16:00:00Z",
|
||||||
updatedAt: "2025-02-05T10:15:45Z",
|
// updatedAt: "2025-02-05T10:15:45Z",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "4",
|
// id: "4",
|
||||||
name: "Email Service",
|
// name: "Email Service",
|
||||||
clientId: "email-svc-333",
|
// clientId: "email-svc-333",
|
||||||
isActive: false,
|
// isActive: false,
|
||||||
createdAt: "2023-07-10T13:00:00Z",
|
// createdAt: "2023-07-10T13:00:00Z",
|
||||||
updatedAt: "2024-12-31T09:25:00Z",
|
// updatedAt: "2024-12-31T09:25:00Z",
|
||||||
},
|
// },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
const ApiServicesPage: FC = () => {
|
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 (
|
return (
|
||||||
<div className="overflow-x-auto rounded shadow-md dark:shadow-gray-800">
|
<div className="relative overflow-x-auto flex flex-col w-full h-full">
|
||||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
<div className="p-4">
|
||||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
<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>
|
<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
|
Name
|
||||||
</th>
|
</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
|
Client ID
|
||||||
</th>
|
</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
|
Is Active
|
||||||
</th>
|
</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
|
Created At
|
||||||
</th>
|
</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
|
Updated At
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{services.map((service) => (
|
{!loading && apiServices.length === 0 ? (
|
||||||
<tr
|
<tr>
|
||||||
key={service.id}
|
<td
|
||||||
className="hover:bg-gray-50 dark:hover:bg-gray-800"
|
colSpan={5}
|
||||||
>
|
className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
|
||||||
<td className="px-6 py-4 text-sm font-medium text-blue-600">
|
>
|
||||||
<Link
|
No services found.
|
||||||
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()}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@ const PersonalInfoPage: FC = () => {
|
|||||||
const profile = useAuth((state) => state.profile);
|
const profile = useAuth((state) => state.profile);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="p-4 sm:p-8">
|
||||||
<h1 className="dark:text-gray-200 text-gray-800 text-2xl">
|
<h1 className="dark:text-gray-200 text-gray-800 text-2xl">
|
||||||
Your profile info in Home services
|
Your profile info in Home services
|
||||||
</h1>
|
</h1>
|
||||||
@ -61,7 +61,7 @@ const PersonalInfoPage: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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;
|
updated_at: string;
|
||||||
created_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,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src", "globals.d.ts"]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user