Compare commits
21 Commits
243b7cce33
...
e85b23b3e8
Author | SHA1 | Date | |
---|---|---|---|
e85b23b3e8 | |||
6164b77bee | |||
8b5a5744ab | |||
d9e9c5ab38 | |||
0dcef81b59 | |||
426b70a1de | |||
912973cdb5 | |||
e4ff799f05 | |||
320715f5aa | |||
a3b04b6243 | |||
f610d7480f | |||
11ac92a026 | |||
98ae3e06e9 | |||
a1146ce371 | |||
a67ec7e78c | |||
9895392b50 | |||
c6998f33e1 | |||
81659181e4 | |||
849b5935c2 | |||
c27d837ab0 | |||
92e9b87227 |
@ -11,14 +11,13 @@ import (
|
||||
"gitea.local/admin/hspguard/internal/web"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type ApiServiceDTO struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ClientID string `json:"client_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Description *string `json:"description"`
|
||||
RedirectUris []string `json:"redirect_uris"`
|
||||
Scopes []string `json:"scopes"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
@ -32,7 +31,7 @@ func NewApiServiceDTO(service repository.ApiService) ApiServiceDTO {
|
||||
ID: service.ID,
|
||||
ClientID: service.ClientID,
|
||||
Name: service.Name,
|
||||
Description: service.Description.String,
|
||||
Description: service.Description,
|
||||
RedirectUris: service.RedirectUris,
|
||||
Scopes: service.Scopes,
|
||||
GrantTypes: service.GrantTypes,
|
||||
@ -129,10 +128,7 @@ func (h *AdminHandler) AddApiService(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if req.Description != "" {
|
||||
params.Description = pgtype.Text{
|
||||
String: req.Description,
|
||||
Valid: true,
|
||||
}
|
||||
params.Description = &req.Description
|
||||
}
|
||||
|
||||
service, err := h.repo.CreateApiService(r.Context(), params)
|
||||
@ -262,12 +258,9 @@ func (h *AdminHandler) UpdateApiService(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
updated, err := h.repo.UpdateApiService(r.Context(), repository.UpdateApiServiceParams{
|
||||
ClientID: service.ClientID,
|
||||
Name: req.Name,
|
||||
Description: pgtype.Text{
|
||||
String: req.Description,
|
||||
Valid: true,
|
||||
},
|
||||
ClientID: service.ClientID,
|
||||
Name: req.Name,
|
||||
Description: &req.Description,
|
||||
RedirectUris: req.RedirectUris,
|
||||
Scopes: req.Scopes,
|
||||
GrantTypes: req.GrantTypes,
|
||||
|
@ -31,5 +31,8 @@ func (h *AdminHandler) RegisterRoutes(router chi.Router) {
|
||||
r.Patch("/api-services/{id}", h.RegenerateApiServiceSecret)
|
||||
r.Put("/api-services/{id}", h.UpdateApiService)
|
||||
r.Patch("/api-services/toggle/{id}", h.ToggleApiService)
|
||||
|
||||
r.Get("/users", h.GetUsers)
|
||||
r.Get("/users/{id}", h.GetUser)
|
||||
})
|
||||
}
|
||||
|
77
internal/admin/users.go
Normal file
77
internal/admin/users.go
Normal file
@ -0,0 +1,77 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"gitea.local/admin/hspguard/internal/types"
|
||||
"gitea.local/admin/hspguard/internal/web"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func NewUserDTO(row *repository.User) types.UserDTO {
|
||||
return types.UserDTO{
|
||||
ID: row.ID,
|
||||
Email: row.Email,
|
||||
FullName: row.FullName,
|
||||
IsAdmin: row.IsAdmin,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
LastLogin: row.LastLogin,
|
||||
PhoneNumber: row.PhoneNumber,
|
||||
ProfilePicture: row.ProfilePicture,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := h.repo.FindAllUsers(r.Context())
|
||||
if err != nil {
|
||||
log.Println("ERR: Failed to query users from db:", err)
|
||||
web.Error(w, "failed to get all users", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Items []types.UserDTO `json:"items"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
var items []types.UserDTO
|
||||
|
||||
for _, user := range users {
|
||||
items = append(items, NewUserDTO(&user))
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
|
||||
if err := encoder.Encode(&Response{
|
||||
Items: items,
|
||||
Count: len(items),
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to send response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
userId := chi.URLParam(r, "id")
|
||||
parsed, err := uuid.Parse(userId)
|
||||
if err != nil {
|
||||
web.Error(w, "user id provided is not a valid uuid", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.FindUserId(r.Context(), parsed)
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided id not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
|
||||
if err := encoder.Encode(NewUserDTO(&user)); err != nil {
|
||||
web.Error(w, "failed to encode user dto", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ 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.Second)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
|
||||
},
|
||||
}
|
||||
|
||||
@ -160,16 +160,16 @@ func (h *AuthHandler) getProfile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": user.ID.String(),
|
||||
"full_name": user.FullName,
|
||||
"email": user.Email,
|
||||
"phone_number": user.PhoneNumber,
|
||||
"isAdmin": user.IsAdmin,
|
||||
"last_login": user.LastLogin,
|
||||
"profile_picture": user.ProfilePicture.String,
|
||||
"updated_at": user.UpdatedAt,
|
||||
"created_at": user.CreatedAt,
|
||||
if err := json.NewEncoder(w).Encode(types.UserDTO{
|
||||
ID: user.ID,
|
||||
FullName: user.FullName,
|
||||
Email: user.Email,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
IsAdmin: user.IsAdmin,
|
||||
LastLogin: user.LastLogin,
|
||||
ProfilePicture: user.ProfilePicture,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to encode user profile", http.StatusInternalServerError)
|
||||
}
|
||||
@ -219,10 +219,10 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
AccessToken string `json:"access"`
|
||||
RefreshToken string `json:"refresh"`
|
||||
// fields required for UI in account selector, e.g. email, full name and avatar
|
||||
FullName string `json:"full_name"`
|
||||
Email string `json:"email"`
|
||||
Id string `json:"id"`
|
||||
ProfilePicture string `json:"profile_picture"`
|
||||
FullName string `json:"full_name"`
|
||||
Email string `json:"email"`
|
||||
Id string `json:"id"`
|
||||
ProfilePicture *string `json:"profile_picture"`
|
||||
// Avatar
|
||||
}
|
||||
|
||||
@ -234,7 +234,7 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
FullName: user.FullName,
|
||||
Email: user.Email,
|
||||
Id: user.ID.String(),
|
||||
ProfilePicture: user.ProfilePicture.String,
|
||||
ProfilePicture: user.ProfilePicture,
|
||||
// Avatar
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
|
@ -76,14 +76,20 @@ func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var roles = []string{"user"}
|
||||
|
||||
if user.IsAdmin {
|
||||
roles = append(roles, "admin")
|
||||
}
|
||||
|
||||
claims := types.ApiClaims{
|
||||
Email: user.Email,
|
||||
// TODO:
|
||||
EmailVerified: true,
|
||||
Name: user.FullName,
|
||||
Picture: user.ProfilePicture.String,
|
||||
Picture: user.ProfilePicture,
|
||||
Nonce: nonce,
|
||||
Roles: []string{"user", "admin"},
|
||||
Roles: roles,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: h.cfg.Jwt.Issuer,
|
||||
// TODO: use dedicated API id that is in local DB and bind to user there
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const activateApiService = `-- name: ActivateApiService :exec
|
||||
@ -33,14 +32,14 @@ INSERT INTO api_services (
|
||||
`
|
||||
|
||||
type CreateApiServiceParams struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
RedirectUris []string `json:"redirect_uris"`
|
||||
Scopes []string `json:"scopes"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
RedirectUris []string `json:"redirect_uris"`
|
||||
Scopes []string `json:"scopes"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateApiService(ctx context.Context, arg CreateApiServiceParams) (ApiService, error) {
|
||||
@ -185,12 +184,12 @@ RETURNING id, client_id, client_secret, name, redirect_uris, scopes, grant_types
|
||||
`
|
||||
|
||||
type UpdateApiServiceParams struct {
|
||||
ClientID string `json:"client_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
RedirectUris []string `json:"redirect_uris"`
|
||||
Scopes []string `json:"scopes"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
ClientID string `json:"client_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
RedirectUris []string `json:"redirect_uris"`
|
||||
Scopes []string `json:"scopes"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateApiService(ctx context.Context, arg UpdateApiServiceParams) (ApiService, error) {
|
||||
|
@ -8,32 +8,31 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type ApiService struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
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"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
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"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"full_name"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
LastLogin pgtype.Timestamptz `json:"last_login"`
|
||||
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||
ProfilePicture pgtype.Text `json:"profile_picture"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"full_name"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
PhoneNumber *string `json:"phone_number"`
|
||||
ProfilePicture *string `json:"profile_picture"`
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const findAllUsers = `-- name: FindAllUsers :many
|
||||
@ -126,8 +125,8 @@ WHERE id = $2
|
||||
`
|
||||
|
||||
type UpdateProfilePictureParams struct {
|
||||
ProfilePicture pgtype.Text `json:"profile_picture"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
ProfilePicture *string `json:"profile_picture"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateProfilePicture(ctx context.Context, arg UpdateProfilePictureParams) error {
|
||||
|
@ -12,7 +12,7 @@ type ApiClaims struct {
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Name string `json:"name"`
|
||||
Picture string `json:"picture"`
|
||||
Picture *string `json:"picture"`
|
||||
Nonce string `json:"nonce"`
|
||||
Roles []string `json:"roles"`
|
||||
// TODO: add given_name, family_name, locale...
|
||||
|
19
internal/types/user.go
Normal file
19
internal/types/user.go
Normal file
@ -0,0 +1,19 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserDTO struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"full_name"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
PhoneNumber *string `json:"phone_number"`
|
||||
ProfilePicture *string `json:"profile_picture"`
|
||||
}
|
@ -19,7 +19,6 @@ import (
|
||||
"gitea.local/admin/hspguard/internal/web"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/minio/minio-go/v7"
|
||||
)
|
||||
|
||||
@ -170,11 +169,8 @@ func (h *UserHandler) uploadAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err := h.repo.UpdateProfilePicture(r.Context(), repository.UpdateProfilePictureParams{
|
||||
ProfilePicture: pgtype.Text{
|
||||
String: uploadInfo.Key,
|
||||
Valid: true,
|
||||
},
|
||||
ID: user.ID,
|
||||
ProfilePicture: &uploadInfo.Key,
|
||||
ID: user.ID,
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to update profile picture", http.StatusInternalServerError)
|
||||
return
|
||||
|
92
sqlc.yaml
92
sqlc.yaml
@ -1,4 +1,3 @@
|
||||
|
||||
version: "2"
|
||||
sql:
|
||||
- engine: "postgresql"
|
||||
@ -14,8 +13,95 @@ sql:
|
||||
- db_type: "uuid"
|
||||
go_type:
|
||||
import: "github.com/google/uuid"
|
||||
type: "UUID"
|
||||
- db_type: "timestamptz"
|
||||
type: UUID
|
||||
- db_type: "uuid"
|
||||
nullable: true
|
||||
go_type:
|
||||
import: "github.com/google/uuid"
|
||||
type: UUID
|
||||
pointer: true
|
||||
# ───── bool ──────────────────────────────────────────
|
||||
- db_type: "pg_catalog.bool" # or just "bool"
|
||||
go_type: { type: "bool" }
|
||||
- db_type: "bool" # or just "bool"
|
||||
go_type: { type: "bool" }
|
||||
|
||||
- db_type: "pg_catalog.bool"
|
||||
nullable: true
|
||||
go_type:
|
||||
type: "bool"
|
||||
pointer: true # ⇒ *bool for NULLable columns
|
||||
|
||||
- db_type: "bool"
|
||||
nullable: true
|
||||
go_type:
|
||||
type: "bool"
|
||||
pointer: true # ⇒ *bool for NULLable columns
|
||||
|
||||
# ───── text ──────────────────────────────────────────
|
||||
- db_type: "pg_catalog.text"
|
||||
go_type: { type: "string" }
|
||||
- db_type: "text" # or just "bool"
|
||||
go_type: { type: "string" }
|
||||
|
||||
- db_type: "pg_catalog.text"
|
||||
nullable: true
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true # ⇒ *bool for NULLable columns
|
||||
|
||||
- db_type: "text"
|
||||
nullable: true
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true # ⇒ *bool for NULLable columns
|
||||
|
||||
# ───── timestamp (WITHOUT TZ) ────────────────────────
|
||||
- db_type: "pg_catalog.timestamp" # or "timestamp"
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
|
||||
- db_type: "timestamp" # or "timestamp"
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
|
||||
- db_type: "pg_catalog.timestamp"
|
||||
nullable: true
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
pointer: true
|
||||
|
||||
- db_type: "timestamp"
|
||||
nullable: true
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
pointer: true
|
||||
|
||||
# ───── timestamptz (WITH TZ) ─────────────────────────
|
||||
- db_type: "pg_catalog.timestamptz" # or "timestamptz"
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
|
||||
- db_type: "timestamptz" # or "timestamptz"
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
|
||||
- db_type: "pg_catalog.timestamptz"
|
||||
nullable: true
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
pointer: true
|
||||
|
||||
- db_type: "timestamptz"
|
||||
nullable: true
|
||||
go_type:
|
||||
import: "time"
|
||||
type: "Time"
|
||||
pointer: true
|
||||
|
@ -9,13 +9,15 @@ import AuthenticatePage from "./pages/Authenticate";
|
||||
import AuthLayout from "./layout/AuthLayout";
|
||||
import DashboardLayout from "./layout/DashboardLayout";
|
||||
import PersonalInfoPage from "./pages/PersonalInfo";
|
||||
import ApiServicesPage from "./pages/ApiServices";
|
||||
import ApiServicesPage from "./pages/Admin/ApiServices";
|
||||
import AdminLayout from "./layout/AdminLayout";
|
||||
import ApiServiceCreatePage from "./pages/ApiServices/Create";
|
||||
import ViewApiServicePage from "./pages/ApiServices/View";
|
||||
import ApiServiceCreatePage from "./pages/Admin/ApiServices/Create";
|
||||
import ViewApiServicePage from "./pages/Admin/ApiServices/View";
|
||||
import NotAllowedPage from "./pages/NotAllowed";
|
||||
import NotFoundPage from "./pages/NotFound";
|
||||
import ApiServiceEditPage from "./pages/ApiServices/Update";
|
||||
import ApiServiceEditPage from "./pages/Admin/ApiServices/Update";
|
||||
import AdminUsersPage from "./pages/Admin/Users";
|
||||
import AdminViewUserPage from "./pages/Admin/Users/View";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@ -53,6 +55,21 @@ const router = createBrowserRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "users",
|
||||
children: [
|
||||
{ index: true, element: <AdminUsersPage /> },
|
||||
// { path: "create", element: <ApiServiceCreatePage /> },
|
||||
{
|
||||
path: "view/:userId",
|
||||
element: <AdminViewUserPage />,
|
||||
},
|
||||
// {
|
||||
// path: "edit/:serviceId",
|
||||
// element: <ApiServiceEditPage />,
|
||||
// },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
31
web/src/api/admin/users.ts
Normal file
31
web/src/api/admin/users.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { UserProfile } from "@/types";
|
||||
import { axios, handleApiError } from "..";
|
||||
|
||||
export interface FetchUsersResponse {
|
||||
items: UserProfile[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const adminGetUsersApi = async (): Promise<FetchUsersResponse> => {
|
||||
const response = await axios.get<FetchUsersResponse>("/api/v1/admin/users");
|
||||
|
||||
if (response.status !== 200 && response.status !== 201)
|
||||
throw await handleApiError(response);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export type FetchUserResponse = UserProfile;
|
||||
|
||||
export const adminGetUserApi = async (
|
||||
id: string,
|
||||
): Promise<FetchUserResponse> => {
|
||||
const response = await axios.get<FetchUserResponse>(
|
||||
`/api/v1/admin/users/${id}`,
|
||||
);
|
||||
|
||||
if (response.status !== 200 && response.status !== 201)
|
||||
throw await handleApiError(response);
|
||||
|
||||
return response.data;
|
||||
};
|
@ -2,7 +2,7 @@ import { createPortal } from "react-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { X } from "lucide-react";
|
||||
import { useAdmin } from "@/store/admin";
|
||||
import { useApiServices } from "@/store/admin/apiServices";
|
||||
import type { ApiServiceCredentials } from "@/types";
|
||||
|
||||
const download = (credentials: ApiServiceCredentials) => {
|
||||
@ -19,8 +19,8 @@ const download = (credentials: ApiServiceCredentials) => {
|
||||
};
|
||||
|
||||
const ApiServiceCredentialsModal = () => {
|
||||
const credentials = useAdmin((state) => state.createdCredentials);
|
||||
const resetCredentials = useAdmin((state) => state.resetCredentials);
|
||||
const credentials = useApiServices((state) => state.createdCredentials);
|
||||
const resetCredentials = useApiServices((state) => state.resetCredentials);
|
||||
|
||||
const portalRoot = document.getElementById("portal-root");
|
||||
if (!portalRoot || !credentials) return null;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { createPortal } from "react-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CircleCheckBig, X } from "lucide-react";
|
||||
import { useAdmin } from "@/store/admin";
|
||||
import { useApiServices } from "@/store/admin/apiServices";
|
||||
|
||||
const ApiServiceUpdatedModal = () => {
|
||||
const resetUpdated = useAdmin((state) => state.resetUpdatedApiService);
|
||||
const resetUpdated = useApiServices((state) => state.resetUpdated);
|
||||
|
||||
const portalRoot = document.getElementById("portal-root");
|
||||
if (!portalRoot) return null;
|
||||
|
@ -27,7 +27,7 @@ const Avatar: FC<AvatarProps> = ({ iconSize = 32, className, avatarId }) => {
|
||||
alt="profile"
|
||||
/>
|
||||
) : (
|
||||
<User size={iconSize} />
|
||||
<User size={iconSize} className="text-gray-800" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -46,7 +46,7 @@ export const useBarItems = (): [BarItem[], (item: BarItem) => boolean] => {
|
||||
tab: "data-personalization",
|
||||
pathname: "/data-personalize",
|
||||
},
|
||||
...(profile.isAdmin
|
||||
...(profile.is_admin
|
||||
? [
|
||||
{
|
||||
icon: <Blocks />,
|
||||
|
@ -32,7 +32,7 @@ const AdminLayout: FC = () => {
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (!profile?.isAdmin) {
|
||||
if (!profile?.is_admin) {
|
||||
return <Navigate to="/not-allowed" />;
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,8 @@ export interface IBackgroundLayoutProps {
|
||||
|
||||
const BackgroundLayout: FC<IBackgroundLayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<div className="relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]">
|
||||
// <div className="relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]">
|
||||
<div className="relative min-h-screen bg-[url(/overlay.jpg)] bg-[#f8f9fb] dark:bg-gradient-to-br from-[#101112] to-[#041758]">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import Breadcrumbs from "@/components/ui/breadcrumbs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import ApiServiceCredentialsModal from "@/feature/ApiServiceCredentialsModal";
|
||||
import { useAdmin } from "@/store/admin";
|
||||
import { useApiServices } from "@/store/admin/apiServices";
|
||||
import { useCallback, type FC } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link } from "react-router";
|
||||
@ -28,9 +28,9 @@ const ApiServiceCreatePage: FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const createApiService = useAdmin((state) => state.createApiService);
|
||||
const createApiService = useApiServices((state) => state.create);
|
||||
|
||||
const credentials = useAdmin((state) => state.createdCredentials);
|
||||
const credentials = useApiServices((state) => state.createdCredentials);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data: FormData) => {
|
@ -2,7 +2,7 @@ import Breadcrumbs from "@/components/ui/breadcrumbs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import ApiServiceUpdatedModal from "@/feature/ApiServiceUpdatedModal";
|
||||
import { useAdmin } from "@/store/admin";
|
||||
import { useApiServices } from "@/store/admin/apiServices";
|
||||
import { useCallback, useEffect, type FC } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, useParams } from "react-router";
|
||||
@ -28,13 +28,13 @@ const ApiServiceEditPage: FC = () => {
|
||||
});
|
||||
|
||||
const { serviceId } = useParams();
|
||||
const apiService = useAdmin((state) => state.viewApiService);
|
||||
const apiService = useApiServices((state) => state.view);
|
||||
|
||||
const loadService = useAdmin((state) => state.fetchApiService);
|
||||
const loadService = useApiServices((state) => state.fetch);
|
||||
|
||||
const updateApiService = useAdmin((state) => state.updateApiService);
|
||||
const updating = useAdmin((state) => state.updatingApiService);
|
||||
const updated = useAdmin((state) => state.updatedApiService);
|
||||
const updateApiService = useApiServices((state) => state.update);
|
||||
const updating = useApiServices((state) => state.updating);
|
||||
const updated = useApiServices((state) => state.updated);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data: FormData) => {
|
@ -1,7 +1,7 @@
|
||||
import Breadcrumbs from "@/components/ui/breadcrumbs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useAdmin } from "@/store/admin";
|
||||
import { useApiServices } from "@/store/admin/apiServices";
|
||||
import { useEffect, type FC } from "react";
|
||||
import { Link, useParams } from "react-router";
|
||||
|
||||
@ -24,13 +24,13 @@ const InfoCard = ({
|
||||
|
||||
const ViewApiServicePage: FC = () => {
|
||||
const { serviceId } = useParams();
|
||||
const apiService = useAdmin((state) => state.viewApiService);
|
||||
// const loading = useAdmin((state) => state.fetchingApiService);
|
||||
const apiService = useApiServices((state) => state.view);
|
||||
// const loading = useApiServices((state) => state.fetchingApiService);
|
||||
|
||||
const loadService = useAdmin((state) => state.fetchApiService);
|
||||
const loadService = useApiServices((state) => state.fetchSingle);
|
||||
|
||||
const toggling = useAdmin((state) => state.togglingApiService);
|
||||
const toggle = useAdmin((state) => state.toggleApiService);
|
||||
const toggling = useApiServices((state) => state.toggling);
|
||||
const toggle = useApiServices((state) => state.toggle);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof serviceId === "string") loadService(serviceId);
|
@ -1,14 +1,14 @@
|
||||
import Breadcrumbs from "@/components/ui/breadcrumbs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAdmin } from "@/store/admin";
|
||||
import { useApiServices } from "@/store/admin/apiServices";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useEffect, type FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const ApiServicesPage: FC = () => {
|
||||
const apiServices = useAdmin((state) => state.apiServices);
|
||||
const loading = useAdmin((state) => state.loadingApiServices);
|
||||
const fetchApiServices = useAdmin((state) => state.fetchApiServices);
|
||||
const apiServices = useApiServices((state) => state.apiServices);
|
||||
const loading = useApiServices((state) => state.loading);
|
||||
const fetchApiServices = useApiServices((state) => state.fetch);
|
||||
|
||||
useEffect(() => {
|
||||
fetchApiServices();
|
139
web/src/pages/Admin/Users/View/index.tsx
Normal file
139
web/src/pages/Admin/Users/View/index.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import Breadcrumbs from "@/components/ui/breadcrumbs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Avatar from "@/feature/Avatar";
|
||||
import { useUsers } from "@/store/admin/users";
|
||||
import { useEffect, type FC } from "react";
|
||||
import { Link, useParams } from "react-router";
|
||||
|
||||
const InfoCard = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="border dark:border-gray-800 border-gray-300 rounded mb-4">
|
||||
<div className="p-4 border-b dark:border-gray-800 border-gray-300">
|
||||
<h2 className="text-gray-800 dark:text-gray-200 font-semibold text-lg">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AdminViewUserPage: FC = () => {
|
||||
const { userId } = useParams();
|
||||
const user = useUsers((state) => state.current);
|
||||
// const loading = useApiServices((state) => state.fetchingApiService);
|
||||
|
||||
const loadUser = useUsers((state) => state.fetchUser);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof userId === "string") loadUser(userId);
|
||||
}, [loadUser, userId]);
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="p-4 flex items-center justify-center h-[60vh]">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent mx-auto mb-3" />
|
||||
<p className="text-gray-600 dark:text-gray-400">Loading User...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dark:text-gray-200 text-gray-800 p-4">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ href: "/admin", label: "Admin" },
|
||||
{ href: "/admin/users", label: "Users" },
|
||||
{ label: "View User" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="sm:p-4 pt-4">
|
||||
{/* 📋 Main Details */}
|
||||
<InfoCard title="Personal Info">
|
||||
<div className="flex flex-col gap-4 text-sm">
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
Avatar:
|
||||
</span>
|
||||
<Avatar
|
||||
avatarId={user.profile_picture ?? undefined}
|
||||
className="w-16 h-16"
|
||||
iconSize={28}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
Full Name:
|
||||
</span>{" "}
|
||||
{user.full_name}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
Email:
|
||||
</span>{" "}
|
||||
{user.email}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
Phone Number:
|
||||
</span>{" "}
|
||||
{user.phone_number || "-"}{" "}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
Is Admin:
|
||||
</span>{" "}
|
||||
<span
|
||||
className={`font-semibold px-2 py-1 rounded ${
|
||||
user.is_admin
|
||||
? "bg-green-200 text-green-800 dark:bg-green-700/20 dark:text-green-300"
|
||||
: "bg-red-200 text-red-800 dark:bg-red-700/20 dark:text-red-300"
|
||||
}`}
|
||||
>
|
||||
{user.is_admin ? "Yes" : "No"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
Created At:
|
||||
</span>{" "}
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
Last Login At:
|
||||
</span>{" "}
|
||||
{user.last_login
|
||||
? new Date(user.last_login).toLocaleString()
|
||||
: "never"}
|
||||
</div>
|
||||
</div>
|
||||
</InfoCard>
|
||||
|
||||
{/* 🚀 Actions */}
|
||||
<div className="flex flex-wrap gap-4 mt-6 justify-between items-center">
|
||||
<Link to="/admin/users">
|
||||
<Button variant="outlined">Back</Button>
|
||||
</Link>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<Link
|
||||
to={`/admin/users/edit/${userId}`}
|
||||
className="hover:underline hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<Button variant="contained">Edit</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminViewUserPage;
|
135
web/src/pages/Admin/Users/index.tsx
Normal file
135
web/src/pages/Admin/Users/index.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import Breadcrumbs from "@/components/ui/breadcrumbs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Avatar from "@/feature/Avatar";
|
||||
import { useUsers } from "@/store/admin/users";
|
||||
import { UserPlus } from "lucide-react";
|
||||
import { useEffect, type FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
const AdminUsersPage: FC = () => {
|
||||
const users = useUsers((state) => state.users);
|
||||
const loading = useUsers((state) => state.fetching);
|
||||
const fetchUsers = useUsers((state) => state.fetchUsers);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, [fetchUsers]);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-stretch w-full h-full">
|
||||
<div className="p-4">
|
||||
<Breadcrumbs
|
||||
className="pb-2"
|
||||
items={[
|
||||
{
|
||||
href: "/admin",
|
||||
label: "Admin",
|
||||
},
|
||||
{
|
||||
label: "Users",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 flex flex-row items-center justify-between">
|
||||
<p className="text-gray-800 dark:text-gray-300">Search...</p>
|
||||
<Link to="/admin/api-services/create">
|
||||
<Button className="flex flex-row items-center gap-2">
|
||||
<UserPlus /> Add User
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-x-auto">
|
||||
<table className="relative min-w-full border-l-0 border border-gray-300 dark:border-gray-700 border-collapse divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{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>
|
||||
)}
|
||||
|
||||
<thead className="bg-black/5 dark:bg-white/5 text-nowrap">
|
||||
<tr>
|
||||
<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-300 dark:border-gray-700">
|
||||
Full Name
|
||||
</th>
|
||||
<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-300 dark:border-gray-700">
|
||||
Email
|
||||
</th>
|
||||
<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-300 dark:border-gray-700">
|
||||
Is Admin
|
||||
</th>
|
||||
<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-300 dark:border-gray-700">
|
||||
Created At
|
||||
</th>
|
||||
<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-300 dark:border-gray-700">
|
||||
Last Login
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{!loading && users.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>
|
||||
) : (
|
||||
users.map((user) => (
|
||||
<tr
|
||||
key={user.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-300 dark:border-gray-700">
|
||||
<Link
|
||||
to={`/admin/users/view/${user.id}`}
|
||||
className="hover:underline hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<Avatar
|
||||
iconSize={21}
|
||||
className="w-8 h-8"
|
||||
avatarId={user.profile_picture ?? undefined}
|
||||
/>
|
||||
<p>{user.full_name}</p>
|
||||
</div>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-700">
|
||||
{user.email}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm border border-gray-300 dark:border-gray-700">
|
||||
<span
|
||||
className={`inline-block px-2 py-1 text-xs rounded-full font-semibold ${
|
||||
user.is_admin
|
||||
? "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"
|
||||
}`}
|
||||
>
|
||||
{user.is_admin ? "Yes" : "No"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 border border-gray-300 dark:border-gray-700">
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 border border-gray-300 dark:border-gray-700">
|
||||
{user.last_login
|
||||
? new Date(user.last_login).toLocaleString()
|
||||
: "never"}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminUsersPage;
|
@ -1,127 +0,0 @@
|
||||
import {
|
||||
getApiService,
|
||||
getApiServices,
|
||||
patchToggleApiService,
|
||||
postApiService,
|
||||
putApiService,
|
||||
type CreateApiServiceRequest,
|
||||
type UpdateApiServiceRequest,
|
||||
} from "@/api/admin/apiServices";
|
||||
import type { ApiService, ApiServiceCredentials } from "@/types";
|
||||
import { create } from "zustand";
|
||||
|
||||
interface IAdminState {
|
||||
apiServices: ApiService[];
|
||||
loadingApiServices: boolean;
|
||||
|
||||
createdCredentials: ApiServiceCredentials | null;
|
||||
creatingApiService: boolean;
|
||||
|
||||
viewApiService: ApiService | null;
|
||||
fetchingApiService: boolean;
|
||||
|
||||
fetchApiServices: () => Promise<void>;
|
||||
fetchApiService: (id: string) => Promise<void>;
|
||||
createApiService: (req: CreateApiServiceRequest) => Promise<void>;
|
||||
resetCredentials: () => void;
|
||||
|
||||
togglingApiService: boolean;
|
||||
toggleApiService: () => Promise<void>;
|
||||
|
||||
updateApiService: (req: UpdateApiServiceRequest) => Promise<void>;
|
||||
updatingApiService: boolean;
|
||||
updatedApiService: boolean;
|
||||
|
||||
resetUpdatedApiService: () => void;
|
||||
}
|
||||
|
||||
export const useAdmin = create<IAdminState>((set, get) => ({
|
||||
apiServices: [],
|
||||
loadingApiServices: false,
|
||||
|
||||
createdCredentials: null,
|
||||
creatingApiService: false,
|
||||
|
||||
viewApiService: null,
|
||||
fetchingApiService: false,
|
||||
|
||||
togglingApiService: false,
|
||||
|
||||
updatingApiService: false,
|
||||
updatedApiService: false,
|
||||
resetUpdatedApiService: () => set({ updatedApiService: false }),
|
||||
|
||||
resetCredentials: () => set({ createdCredentials: null }),
|
||||
|
||||
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 });
|
||||
}
|
||||
},
|
||||
|
||||
fetchApiService: async (id: string) => {
|
||||
set({ fetchingApiService: true });
|
||||
|
||||
try {
|
||||
const response = await getApiService(id);
|
||||
set({ viewApiService: response });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to fetch services:", err);
|
||||
} finally {
|
||||
set({ fetchingApiService: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateApiService: async (req: UpdateApiServiceRequest) => {
|
||||
const viewService = get().viewApiService;
|
||||
if (!viewService) return;
|
||||
|
||||
set({ updatingApiService: true });
|
||||
|
||||
try {
|
||||
await putApiService(viewService.id, req);
|
||||
get().fetchApiService(viewService.id);
|
||||
set({ updatedApiService: true });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to toggle service:", err);
|
||||
} finally {
|
||||
set({ updatingApiService: false });
|
||||
}
|
||||
},
|
||||
|
||||
toggleApiService: async () => {
|
||||
const viewService = get().viewApiService;
|
||||
if (!viewService) return;
|
||||
|
||||
set({ togglingApiService: true });
|
||||
|
||||
try {
|
||||
await patchToggleApiService(viewService.id);
|
||||
get().fetchApiService(viewService.id);
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to toggle service:", err);
|
||||
} finally {
|
||||
set({ togglingApiService: false });
|
||||
}
|
||||
},
|
||||
|
||||
createApiService: async (req: CreateApiServiceRequest) => {
|
||||
set({ creatingApiService: true });
|
||||
|
||||
try {
|
||||
const response = await postApiService(req);
|
||||
set({ createdCredentials: response.credentials });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to fetch services:", err);
|
||||
} finally {
|
||||
set({ creatingApiService: false });
|
||||
}
|
||||
},
|
||||
}));
|
127
web/src/store/admin/apiServices.ts
Normal file
127
web/src/store/admin/apiServices.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import {
|
||||
getApiService,
|
||||
getApiServices,
|
||||
patchToggleApiService,
|
||||
postApiService,
|
||||
putApiService,
|
||||
type CreateApiServiceRequest,
|
||||
type UpdateApiServiceRequest,
|
||||
} from "@/api/admin/apiServices";
|
||||
import type { ApiService, ApiServiceCredentials } from "@/types";
|
||||
import { create } from "zustand";
|
||||
|
||||
interface IApiServicesState {
|
||||
apiServices: ApiService[];
|
||||
loading: boolean;
|
||||
|
||||
createdCredentials: ApiServiceCredentials | null;
|
||||
creating: boolean;
|
||||
|
||||
view: ApiService | null;
|
||||
fetching: boolean;
|
||||
|
||||
fetch: () => Promise<void>;
|
||||
fetchSingle: (id: string) => Promise<void>;
|
||||
create: (req: CreateApiServiceRequest) => Promise<void>;
|
||||
resetCredentials: () => void;
|
||||
|
||||
toggling: boolean;
|
||||
toggle: () => Promise<void>;
|
||||
|
||||
update: (req: UpdateApiServiceRequest) => Promise<void>;
|
||||
updating: boolean;
|
||||
updated: boolean;
|
||||
|
||||
resetUpdated: () => void;
|
||||
}
|
||||
|
||||
export const useApiServices = create<IApiServicesState>((set, get) => ({
|
||||
apiServices: [],
|
||||
loading: false,
|
||||
|
||||
createdCredentials: null,
|
||||
creating: false,
|
||||
|
||||
view: null,
|
||||
fetching: false,
|
||||
|
||||
toggling: false,
|
||||
|
||||
updating: false,
|
||||
updated: false,
|
||||
resetUpdated: () => set({ updated: false }),
|
||||
|
||||
resetCredentials: () => set({ createdCredentials: null }),
|
||||
|
||||
fetch: async () => {
|
||||
set({ loading: true });
|
||||
|
||||
try {
|
||||
const response = await getApiServices();
|
||||
set({ apiServices: response.items });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to fetch services:", err);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
fetchSingle: async (id: string) => {
|
||||
set({ fetching: true });
|
||||
|
||||
try {
|
||||
const response = await getApiService(id);
|
||||
set({ view: response });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to fetch services:", err);
|
||||
} finally {
|
||||
set({ fetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
update: async (req: UpdateApiServiceRequest) => {
|
||||
const viewService = get().view;
|
||||
if (!viewService) return;
|
||||
|
||||
set({ updating: true });
|
||||
|
||||
try {
|
||||
await putApiService(viewService.id, req);
|
||||
get().fetchSingle(viewService.id);
|
||||
set({ updated: true });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to toggle service:", err);
|
||||
} finally {
|
||||
set({ updating: false });
|
||||
}
|
||||
},
|
||||
|
||||
toggle: async () => {
|
||||
const viewService = get().view;
|
||||
if (!viewService) return;
|
||||
|
||||
set({ toggling: true });
|
||||
|
||||
try {
|
||||
await patchToggleApiService(viewService.id);
|
||||
get().fetchSingle(viewService.id);
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to toggle service:", err);
|
||||
} finally {
|
||||
set({ toggling: false });
|
||||
}
|
||||
},
|
||||
|
||||
create: async (req: CreateApiServiceRequest) => {
|
||||
set({ creating: true });
|
||||
|
||||
try {
|
||||
const response = await postApiService(req);
|
||||
set({ createdCredentials: response.credentials });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to fetch services:", err);
|
||||
} finally {
|
||||
set({ creating: false });
|
||||
}
|
||||
},
|
||||
}));
|
48
web/src/store/admin/users.ts
Normal file
48
web/src/store/admin/users.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { adminGetUserApi, adminGetUsersApi } from "@/api/admin/users";
|
||||
import type { UserProfile } from "@/types";
|
||||
import { create } from "zustand";
|
||||
|
||||
export interface IUsersState {
|
||||
users: UserProfile[];
|
||||
fetching: boolean;
|
||||
|
||||
current: UserProfile | null;
|
||||
fetchingCurrent: boolean;
|
||||
|
||||
fetchUsers: () => Promise<void>;
|
||||
fetchUser: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useUsers = create<IUsersState>((set) => ({
|
||||
users: [],
|
||||
fetching: false,
|
||||
|
||||
current: null,
|
||||
fetchingCurrent: false,
|
||||
|
||||
fetchUsers: async () => {
|
||||
set({ fetching: true });
|
||||
|
||||
try {
|
||||
const response = await adminGetUsersApi();
|
||||
set({ users: response.items });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to fetch users for admin:", err);
|
||||
} finally {
|
||||
set({ fetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
fetchUser: async (id: string) => {
|
||||
set({ fetchingCurrent: true });
|
||||
|
||||
try {
|
||||
const response = await adminGetUserApi(id);
|
||||
set({ current: response });
|
||||
} catch (err) {
|
||||
console.log("ERR: Failed to fetch single user for admin:", err);
|
||||
} finally {
|
||||
set({ fetchingCurrent: false });
|
||||
}
|
||||
},
|
||||
}));
|
@ -3,7 +3,7 @@ export interface UserProfile {
|
||||
full_name: string;
|
||||
email: string;
|
||||
phone_number: string;
|
||||
isAdmin: boolean;
|
||||
is_admin: boolean;
|
||||
last_login: string;
|
||||
profile_picture: string | null;
|
||||
updated_at: string;
|
||||
|
Reference in New Issue
Block a user