Compare commits
7 Commits
9983c51e3a
...
34c152a459
Author | SHA1 | Date | |
---|---|---|---|
34c152a459 | |||
ad09e98bba | |||
d3fd5cba16 | |||
64dbb4368c | |||
cb3a6ddc58 | |||
e774f415d8 | |||
d5a22895e7 |
@ -33,6 +33,7 @@ func (h *AdminHandler) RegisterRoutes(router chi.Router) {
|
|||||||
r.Patch("/api-services/toggle/{id}", h.ToggleApiService)
|
r.Patch("/api-services/toggle/{id}", h.ToggleApiService)
|
||||||
|
|
||||||
r.Get("/users", h.GetUsers)
|
r.Get("/users", h.GetUsers)
|
||||||
|
r.Post("/users", h.CreateUser)
|
||||||
r.Get("/users/{id}", h.GetUser)
|
r.Get("/users/{id}", h.GetUser)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"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/web"
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -79,3 +80,75 @@ func (h *AdminHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
web.Error(w, "failed to encode user dto", http.StatusInternalServerError)
|
web.Error(w, "failed to encode user dto", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateUserRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req CreateUserRequest
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(&req); err != nil {
|
||||||
|
web.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Email == "" {
|
||||||
|
web.Error(w, "email is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.FullName == "" {
|
||||||
|
web.Error(w, "full name is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Password == "" {
|
||||||
|
web.Error(w, "password is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := h.repo.FindUserEmail(r.Context(), req.Email)
|
||||||
|
if err == nil {
|
||||||
|
web.Error(w, "user with provided email already exists", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := util.HashPassword(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to hash password for new user:", err)
|
||||||
|
web.Error(w, "failed to create user account", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := repository.InsertUserParams{
|
||||||
|
Email: req.Email,
|
||||||
|
FullName: req.FullName,
|
||||||
|
PasswordHash: hash,
|
||||||
|
IsAdmin: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("INFO: params for user creation:", params)
|
||||||
|
|
||||||
|
userId, err := h.repo.InsertUser(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to insert user into database:", err)
|
||||||
|
web.Error(w, "failed to create user", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
if err := encoder.Encode(Response{
|
||||||
|
ID: userId.String(),
|
||||||
|
}); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -35,4 +35,6 @@ type User struct {
|
|||||||
LastLogin *time.Time `json:"last_login"`
|
LastLogin *time.Time `json:"last_login"`
|
||||||
PhoneNumber *string `json:"phone_number"`
|
PhoneNumber *string `json:"phone_number"`
|
||||||
ProfilePicture *string `json:"profile_picture"`
|
ProfilePicture *string `json:"profile_picture"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
|
EmailVerified bool `json:"email_verified"`
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const findAllUsers = `-- name: FindAllUsers :many
|
const findAllUsers = `-- name: FindAllUsers :many
|
||||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture FROM users
|
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified FROM users
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) FindAllUsers(ctx context.Context) ([]User, error) {
|
func (q *Queries) FindAllUsers(ctx context.Context) ([]User, error) {
|
||||||
@ -35,6 +35,8 @@ func (q *Queries) FindAllUsers(ctx context.Context) ([]User, error) {
|
|||||||
&i.LastLogin,
|
&i.LastLogin,
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.ProfilePicture,
|
&i.ProfilePicture,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.EmailVerified,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -47,7 +49,7 @@ func (q *Queries) FindAllUsers(ctx context.Context) ([]User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findUserEmail = `-- name: FindUserEmail :one
|
const findUserEmail = `-- name: FindUserEmail :one
|
||||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture FROM users WHERE email = $1 LIMIT 1
|
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified FROM users WHERE email = $1 LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) FindUserEmail(ctx context.Context, email string) (User, error) {
|
func (q *Queries) FindUserEmail(ctx context.Context, email string) (User, error) {
|
||||||
@ -64,12 +66,14 @@ func (q *Queries) FindUserEmail(ctx context.Context, email string) (User, error)
|
|||||||
&i.LastLogin,
|
&i.LastLogin,
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.ProfilePicture,
|
&i.ProfilePicture,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.EmailVerified,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const findUserId = `-- name: FindUserId :one
|
const findUserId = `-- name: FindUserId :one
|
||||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture FROM users WHERE id = $1 LIMIT 1
|
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number, profile_picture, created_by, email_verified FROM users WHERE id = $1 LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) FindUserId(ctx context.Context, id uuid.UUID) (User, error) {
|
func (q *Queries) FindUserId(ctx context.Context, id uuid.UUID) (User, error) {
|
||||||
@ -86,24 +90,27 @@ func (q *Queries) FindUserId(ctx context.Context, id uuid.UUID) (User, error) {
|
|||||||
&i.LastLogin,
|
&i.LastLogin,
|
||||||
&i.PhoneNumber,
|
&i.PhoneNumber,
|
||||||
&i.ProfilePicture,
|
&i.ProfilePicture,
|
||||||
|
&i.CreatedBy,
|
||||||
|
&i.EmailVerified,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertUser = `-- name: InsertUser :one
|
const insertUser = `-- name: InsertUser :one
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
email, full_name, password_hash, is_admin
|
email, full_name, password_hash, is_admin, created_by
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4
|
$1, $2, $3, $4, $5
|
||||||
)
|
)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertUserParams struct {
|
type InsertUserParams struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
PasswordHash string `json:"password_hash"`
|
PasswordHash string `json:"password_hash"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (uuid.UUID, error) {
|
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (uuid.UUID, error) {
|
||||||
@ -112,6 +119,7 @@ func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (uuid.UU
|
|||||||
arg.FullName,
|
arg.FullName,
|
||||||
arg.PasswordHash,
|
arg.PasswordHash,
|
||||||
arg.IsAdmin,
|
arg.IsAdmin,
|
||||||
|
arg.CreatedBy,
|
||||||
)
|
)
|
||||||
var id uuid.UUID
|
var id uuid.UUID
|
||||||
err := row.Scan(&id)
|
err := row.Scan(&id)
|
||||||
|
12
migrations/00006_add_user_creator.sql
Normal file
12
migrations/00006_add_user_creator.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN created_by UUID REFERENCES users (id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- +goose StatementEnd
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN created_by;
|
||||||
|
|
||||||
|
-- +goose StatementEnd
|
12
migrations/00007_add_user_verification.sql
Normal file
12
migrations/00007_add_user_verification.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- +goose StatementEnd
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN email_verified;
|
||||||
|
|
||||||
|
-- +goose StatementEnd
|
@ -3,9 +3,9 @@ SELECT * FROM users;
|
|||||||
|
|
||||||
-- name: InsertUser :one
|
-- name: InsertUser :one
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
email, full_name, password_hash, is_admin
|
email, full_name, password_hash, is_admin, created_by
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4
|
$1, $2, $3, $4, $5
|
||||||
)
|
)
|
||||||
RETURNING id;
|
RETURNING id;
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import NotFoundPage from "./pages/NotFound";
|
|||||||
import ApiServiceEditPage from "./pages/Admin/ApiServices/Update";
|
import ApiServiceEditPage from "./pages/Admin/ApiServices/Update";
|
||||||
import AdminUsersPage from "./pages/Admin/Users";
|
import AdminUsersPage from "./pages/Admin/Users";
|
||||||
import AdminViewUserPage from "./pages/Admin/Users/View";
|
import AdminViewUserPage from "./pages/Admin/Users/View";
|
||||||
|
import AdminCreateUserPage from "./pages/Admin/Users/Create";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -59,7 +60,7 @@ const router = createBrowserRouter([
|
|||||||
path: "users",
|
path: "users",
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <AdminUsersPage /> },
|
{ index: true, element: <AdminUsersPage /> },
|
||||||
// { path: "create", element: <ApiServiceCreatePage /> },
|
{ path: "create", element: <AdminCreateUserPage /> },
|
||||||
{
|
{
|
||||||
path: "view/:userId",
|
path: "view/:userId",
|
||||||
element: <AdminViewUserPage />,
|
element: <AdminViewUserPage />,
|
||||||
|
@ -29,3 +29,28 @@ export const adminGetUserApi = async (
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface CreateUserRequest {
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
password: string;
|
||||||
|
is_admin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUserResponse {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const postUser = async (
|
||||||
|
req: CreateUserRequest,
|
||||||
|
): Promise<CreateUserResponse> => {
|
||||||
|
const response = await axios.post<CreateUserResponse>(
|
||||||
|
"/api/v1/admin/users",
|
||||||
|
req,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200 && response.status !== 201)
|
||||||
|
throw await handleApiError(response);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
188
web/src/pages/Admin/Users/Create/index.tsx
Normal file
188
web/src/pages/Admin/Users/Create/index.tsx
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import Breadcrumbs from "@/components/ui/breadcrumbs";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useUsers } from "@/store/admin/users";
|
||||||
|
import { useCallback, type FC } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
password: string;
|
||||||
|
repeatPassword: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AdminCreateUserPage: FC = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormData>();
|
||||||
|
|
||||||
|
const createUser = useUsers((state) => state.createUser);
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(data: FormData) => {
|
||||||
|
console.log("Form submitted:", data);
|
||||||
|
createUser({
|
||||||
|
email: data.email,
|
||||||
|
full_name: data.fullName,
|
||||||
|
password: data.password,
|
||||||
|
is_admin: data.isAdmin,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[createUser],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<Breadcrumbs
|
||||||
|
items={[
|
||||||
|
{ href: "/admin", label: "Admin" },
|
||||||
|
{ href: "/admin/users", label: "Users" },
|
||||||
|
{ label: "Create new User" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
{/* Personal Information */}
|
||||||
|
<div className="border dark:border-gray-800 border-gray-300 rounded mt-4 flex flex-col">
|
||||||
|
<div className="p-4 border-b dark:border-gray-800 border-gray-300">
|
||||||
|
<h2 className="text-gray-800 dark:text-gray-200">
|
||||||
|
User Personal Info
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex flex-col gap-2 mb-4">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm">Name</p>
|
||||||
|
<Input
|
||||||
|
placeholder="Full Name"
|
||||||
|
{...register("fullName", { required: "Full Name is required" })}
|
||||||
|
/>
|
||||||
|
{errors.fullName && (
|
||||||
|
<span className="text-red-500 text-sm">
|
||||||
|
{errors.fullName.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 mb-4">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||||
|
Email Address
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
placeholder="user@company.org"
|
||||||
|
{...register("email", {
|
||||||
|
required: true,
|
||||||
|
pattern: {
|
||||||
|
value: /\S+@\S+\.\S+/,
|
||||||
|
message: "Invalid email",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
className="dark:text-gray-100 border border-gray-300 dark:border-gray-700 rounded placeholder:text-gray-600 text-sm p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<span className="text-red-500 text-sm">
|
||||||
|
{errors.email.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 mb-4">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||||
|
Password
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
placeholder="secret"
|
||||||
|
{...register("password", {
|
||||||
|
required: true,
|
||||||
|
validate: (password) => {
|
||||||
|
if (password.length < 8) {
|
||||||
|
return "Password must be at least 8 characters long";
|
||||||
|
}
|
||||||
|
if (!password.match(/[a-zA-Z]+/gi)) {
|
||||||
|
return "Password must contain characters";
|
||||||
|
}
|
||||||
|
if (password.split("").every((c) => c.toLowerCase() == c)) {
|
||||||
|
return "Password should contain at least 1 uppercase character";
|
||||||
|
}
|
||||||
|
if (!password.match(/\d+/gi)) {
|
||||||
|
return "Password should contain at least 1 digit";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
className="dark:text-gray-100 border border-gray-300 dark:border-gray-700 rounded placeholder:text-gray-600 text-sm p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<span className="text-red-500 text-sm">
|
||||||
|
{errors.password.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 mb-4">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
||||||
|
Repeat Password
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
placeholder="secret-again"
|
||||||
|
{...register("repeatPassword", {
|
||||||
|
required: true,
|
||||||
|
validate: (repeatPassword, { password }) => {
|
||||||
|
if (repeatPassword != password) {
|
||||||
|
return "Password does not match";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
className="dark:text-gray-100 border border-gray-300 dark:border-gray-700 rounded placeholder:text-gray-600 text-sm p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
{errors.repeatPassword && (
|
||||||
|
<span className="text-red-500 text-sm">
|
||||||
|
{errors.repeatPassword.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Final Section */}
|
||||||
|
<div className="border dark:border-gray-800 border-gray-300 rounded mt-4 flex flex-col">
|
||||||
|
<div className="p-4 border-b dark:border-gray-800 border-gray-300">
|
||||||
|
<h2 className="text-gray-800 dark:text-gray-200">
|
||||||
|
Final Customization & Submit
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<label className="inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="sr-only peer"
|
||||||
|
{...register("isAdmin")}
|
||||||
|
/>
|
||||||
|
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"></div>
|
||||||
|
<span className="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||||
|
Is Admin
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between gap-2 mt-4">
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
<Link to="/admin/users">
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
className="text-red-400 hover:text-red-500"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminCreateUserPage;
|
@ -33,7 +33,7 @@ const AdminUsersPage: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-4 flex flex-row items-center justify-between">
|
<div className="p-4 flex flex-row items-center justify-between">
|
||||||
<p className="text-gray-800 dark:text-gray-300">Search...</p>
|
<p className="text-gray-800 dark:text-gray-300">Search...</p>
|
||||||
<Link to="/admin/api-services/create">
|
<Link to="/admin/users/create">
|
||||||
<Button className="flex flex-row items-center gap-2">
|
<Button className="flex flex-row items-center gap-2">
|
||||||
<UserPlus /> Add User
|
<UserPlus /> Add User
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { adminGetUserApi, adminGetUsersApi } from "@/api/admin/users";
|
import {
|
||||||
|
adminGetUserApi,
|
||||||
|
adminGetUsersApi,
|
||||||
|
postUser,
|
||||||
|
type CreateUserRequest,
|
||||||
|
} from "@/api/admin/users";
|
||||||
import type { UserProfile } from "@/types";
|
import type { UserProfile } from "@/types";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
@ -9,6 +14,9 @@ export interface IUsersState {
|
|||||||
current: UserProfile | null;
|
current: UserProfile | null;
|
||||||
fetchingCurrent: boolean;
|
fetchingCurrent: boolean;
|
||||||
|
|
||||||
|
creating: boolean;
|
||||||
|
createUser: (req: CreateUserRequest) => Promise<void>;
|
||||||
|
|
||||||
fetchUsers: () => Promise<void>;
|
fetchUsers: () => Promise<void>;
|
||||||
fetchUser: (id: string) => Promise<void>;
|
fetchUser: (id: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
@ -17,9 +25,24 @@ export const useUsers = create<IUsersState>((set) => ({
|
|||||||
users: [],
|
users: [],
|
||||||
fetching: false,
|
fetching: false,
|
||||||
|
|
||||||
|
creating: false,
|
||||||
|
|
||||||
current: null,
|
current: null,
|
||||||
fetchingCurrent: false,
|
fetchingCurrent: false,
|
||||||
|
|
||||||
|
createUser: async (req: CreateUserRequest) => {
|
||||||
|
set({ creating: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await postUser(req);
|
||||||
|
console.log("INFO: User has been created:", response);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("ERR: Failed to create user:", err);
|
||||||
|
} finally {
|
||||||
|
set({ creating: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
fetchUsers: async () => {
|
fetchUsers: async () => {
|
||||||
set({ fetching: true });
|
set({ fetching: true });
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user