phos 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/templates/backend/elysia/src/api/user_api.ts +313 -0
- package/src/templates/backend/elysia/src/function/helper.ts +111 -0
- package/src/templates/backend/elysia/src/service/user_service.ts +109 -0
- package/src/templates/backend/elysia/src/sql/user_sql.ts +191 -0
- package/src/templates/backend/elysia/src/types/user_type.ts +54 -0
- package/src/templates/backend/fastapi/src/api/user_api.py +163 -0
- package/src/templates/backend/fastapi/src/function/__init__.py +0 -0
- package/src/templates/backend/fastapi/src/function/helper.py +107 -0
- package/src/templates/backend/fastapi/src/service/user_service.py +94 -0
- package/src/templates/backend/fastapi/src/sql/user_sql.py +197 -0
- package/src/templates/backend/fastapi/src/types/user_type.py +64 -0
- package/src/templates/frontend/vue/.editorconfig +8 -0
- package/src/templates/frontend/vue/.gitattributes +1 -0
- package/src/templates/frontend/vue/.oxlintrc.json +10 -0
- package/src/templates/frontend/vue/.prettierrc.json +6 -0
- package/src/templates/frontend/vue/.vscode/extensions.json +11 -0
- package/src/templates/frontend/vue/README.md +73 -0
- package/src/templates/frontend/vue/e2e/tsconfig.json +4 -0
- package/src/templates/frontend/vue/e2e/vue.spec.ts +8 -0
- package/src/templates/frontend/vue/env.d.ts +1 -0
- package/src/templates/frontend/vue/eslint.config.ts +38 -0
- package/src/templates/frontend/vue/index.html +13 -0
- package/src/templates/frontend/vue/package.json +54 -0
- package/src/templates/frontend/vue/playwright.config.ts +110 -0
- package/src/templates/frontend/vue/public/favicon.ico +0 -0
- package/src/templates/frontend/vue/src/App.vue +11 -0
- package/src/templates/frontend/vue/src/__tests__/App.spec.ts +11 -0
- package/src/templates/frontend/vue/src/main.ts +12 -0
- package/src/templates/frontend/vue/src/router/index.ts +8 -0
- package/src/templates/frontend/vue/src/stores/counter.ts +12 -0
- package/src/templates/frontend/vue/tsconfig.app.json +12 -0
- package/src/templates/frontend/vue/tsconfig.json +14 -0
- package/src/templates/frontend/vue/tsconfig.node.json +19 -0
- package/src/templates/frontend/vue/tsconfig.vitest.json +11 -0
- package/src/templates/frontend/vue/vite.config.ts +20 -0
- package/src/templates/frontend/vue/vitest.config.ts +14 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import db from "../db";
|
|
2
|
+
import bcrypt from "bcrypt";
|
|
3
|
+
import type { User, CreateUserDto, UpdateUserDto } from "../types/user_type";
|
|
4
|
+
|
|
5
|
+
const SALT_ROUNDS = 10;
|
|
6
|
+
|
|
7
|
+
export async function getUsers(): Promise<User[]> {
|
|
8
|
+
const result = await db`
|
|
9
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
10
|
+
FROM users
|
|
11
|
+
WHERE is_active = true
|
|
12
|
+
ORDER BY created_at DESC
|
|
13
|
+
`;
|
|
14
|
+
return result as unknown as User[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function getUserById(id: number): Promise<User | null> {
|
|
18
|
+
const result = await db`
|
|
19
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
20
|
+
FROM users
|
|
21
|
+
WHERE id = ${id}
|
|
22
|
+
`;
|
|
23
|
+
if (result.length === 0) return null;
|
|
24
|
+
return result[0] as unknown as User;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getUserByEmail(email: string): Promise<User | null> {
|
|
28
|
+
const result = await db`
|
|
29
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
30
|
+
FROM users
|
|
31
|
+
WHERE email = ${email}
|
|
32
|
+
`;
|
|
33
|
+
if (result.length === 0) return null;
|
|
34
|
+
return result[0] as unknown as User;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function getUserByUsername(username: string): Promise<User | null> {
|
|
38
|
+
const result = await db`
|
|
39
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
40
|
+
FROM users
|
|
41
|
+
WHERE username = ${username}
|
|
42
|
+
`;
|
|
43
|
+
if (result.length === 0) return null;
|
|
44
|
+
return result[0] as unknown as User;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function createUser(data: CreateUserDto): Promise<User> {
|
|
48
|
+
const hashedPassword = await bcrypt.hash(data.password, SALT_ROUNDS);
|
|
49
|
+
|
|
50
|
+
const result = await db`
|
|
51
|
+
INSERT INTO users (email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at)
|
|
52
|
+
VALUES (
|
|
53
|
+
${data.email},
|
|
54
|
+
${data.username},
|
|
55
|
+
${data.full_name},
|
|
56
|
+
${hashedPassword},
|
|
57
|
+
${data.avatar_url ?? null},
|
|
58
|
+
${data.bio ?? null},
|
|
59
|
+
${data.role ?? "user"},
|
|
60
|
+
true,
|
|
61
|
+
CURRENT_TIMESTAMP,
|
|
62
|
+
CURRENT_TIMESTAMP
|
|
63
|
+
)
|
|
64
|
+
RETURNING id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
65
|
+
`;
|
|
66
|
+
return result[0] as unknown as User;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function updateUser(id: number, data: UpdateUserDto): Promise<User | null> {
|
|
70
|
+
const setParts: string[] = [];
|
|
71
|
+
const values: any[] = [];
|
|
72
|
+
let paramIndex = 1;
|
|
73
|
+
|
|
74
|
+
if (data.email !== undefined) {
|
|
75
|
+
setParts.push(`email = $${paramIndex}`);
|
|
76
|
+
values.push(data.email);
|
|
77
|
+
paramIndex++;
|
|
78
|
+
}
|
|
79
|
+
if (data.username !== undefined) {
|
|
80
|
+
setParts.push(`username = $${paramIndex}`);
|
|
81
|
+
values.push(data.username);
|
|
82
|
+
paramIndex++;
|
|
83
|
+
}
|
|
84
|
+
if (data.full_name !== undefined) {
|
|
85
|
+
setParts.push(`full_name = $${paramIndex}`);
|
|
86
|
+
values.push(data.full_name);
|
|
87
|
+
paramIndex++;
|
|
88
|
+
}
|
|
89
|
+
if (data.password !== undefined) {
|
|
90
|
+
const hashedPassword = await bcrypt.hash(data.password, SALT_ROUNDS);
|
|
91
|
+
setParts.push(`password = $${paramIndex}`);
|
|
92
|
+
values.push(hashedPassword);
|
|
93
|
+
paramIndex++;
|
|
94
|
+
}
|
|
95
|
+
if (data.avatar_url !== undefined) {
|
|
96
|
+
setParts.push(`avatar_url = $${paramIndex}`);
|
|
97
|
+
values.push(data.avatar_url);
|
|
98
|
+
paramIndex++;
|
|
99
|
+
}
|
|
100
|
+
if (data.bio !== undefined) {
|
|
101
|
+
setParts.push(`bio = $${paramIndex}`);
|
|
102
|
+
values.push(data.bio);
|
|
103
|
+
paramIndex++;
|
|
104
|
+
}
|
|
105
|
+
if (data.role !== undefined) {
|
|
106
|
+
setParts.push(`role = $${paramIndex}`);
|
|
107
|
+
values.push(data.role);
|
|
108
|
+
paramIndex++;
|
|
109
|
+
}
|
|
110
|
+
if (data.is_active !== undefined) {
|
|
111
|
+
setParts.push(`is_active = $${paramIndex}`);
|
|
112
|
+
values.push(data.is_active);
|
|
113
|
+
paramIndex++;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (setParts.length === 0) {
|
|
117
|
+
return getUserById(id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setParts.push(`updated_at = CURRENT_TIMESTAMP`);
|
|
121
|
+
const query = `
|
|
122
|
+
UPDATE users
|
|
123
|
+
SET ${setParts.join(", ")}
|
|
124
|
+
WHERE id = $${paramIndex}
|
|
125
|
+
RETURNING id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
126
|
+
`;
|
|
127
|
+
values.push(id);
|
|
128
|
+
|
|
129
|
+
const result = await db.unsafe(query, values);
|
|
130
|
+
if (result.length === 0) return null;
|
|
131
|
+
return result[0] as unknown as User;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function deleteUser(id: number): Promise<boolean> {
|
|
135
|
+
const result = await db`
|
|
136
|
+
DELETE FROM users
|
|
137
|
+
WHERE id = ${id}
|
|
138
|
+
`;
|
|
139
|
+
return result.count > 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function softDeleteUser(id: number): Promise<User | null> {
|
|
143
|
+
const result = await db`
|
|
144
|
+
UPDATE users
|
|
145
|
+
SET is_active = false, updated_at = CURRENT_TIMESTAMP
|
|
146
|
+
WHERE id = ${id}
|
|
147
|
+
RETURNING id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
148
|
+
`;
|
|
149
|
+
if (result.length === 0) return null;
|
|
150
|
+
return result[0] as unknown as User;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function searchUsers(keyword: string): Promise<User[]> {
|
|
154
|
+
const result = await db`
|
|
155
|
+
SELECT id, email, username, full_name, password, avatar_url, bio, role, is_active, created_at, updated_at
|
|
156
|
+
FROM users
|
|
157
|
+
WHERE is_active = true
|
|
158
|
+
AND (
|
|
159
|
+
email ILIKE ${`%${keyword}%`}
|
|
160
|
+
OR username ILIKE ${`%${keyword}%`}
|
|
161
|
+
OR full_name ILIKE ${`%${keyword}%`}
|
|
162
|
+
)
|
|
163
|
+
ORDER BY created_at DESC
|
|
164
|
+
`;
|
|
165
|
+
return result as unknown as User[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {
|
|
169
|
+
return await bcrypt.compare(plainPassword, hashedPassword);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const userTableSchema = `
|
|
173
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
174
|
+
id SERIAL PRIMARY KEY,
|
|
175
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
176
|
+
username VARCHAR(100) UNIQUE NOT NULL,
|
|
177
|
+
full_name VARCHAR(255) NOT NULL,
|
|
178
|
+
password VARCHAR(255) NOT NULL,
|
|
179
|
+
avatar_url TEXT,
|
|
180
|
+
bio TEXT,
|
|
181
|
+
role VARCHAR(50) DEFAULT 'user',
|
|
182
|
+
is_active BOOLEAN DEFAULT true,
|
|
183
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
184
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);
|
|
191
|
+
`;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface User {
|
|
2
|
+
id: number;
|
|
3
|
+
email: string;
|
|
4
|
+
username: string;
|
|
5
|
+
full_name: string;
|
|
6
|
+
password: string;
|
|
7
|
+
avatar_url: string | null;
|
|
8
|
+
bio: string | null;
|
|
9
|
+
role: string;
|
|
10
|
+
is_active: boolean;
|
|
11
|
+
created_at: Date;
|
|
12
|
+
updated_at: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CreateUserDto {
|
|
16
|
+
email: string;
|
|
17
|
+
username: string;
|
|
18
|
+
full_name: string;
|
|
19
|
+
password: string;
|
|
20
|
+
avatar_url?: string | null;
|
|
21
|
+
bio?: string | null;
|
|
22
|
+
role?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UpdateUserDto {
|
|
26
|
+
email?: string;
|
|
27
|
+
username?: string;
|
|
28
|
+
full_name?: string;
|
|
29
|
+
password?: string;
|
|
30
|
+
avatar_url?: string | null;
|
|
31
|
+
bio?: string | null;
|
|
32
|
+
role?: string;
|
|
33
|
+
is_active?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UserResponse {
|
|
37
|
+
id: number;
|
|
38
|
+
email: string;
|
|
39
|
+
username: string;
|
|
40
|
+
full_name: string;
|
|
41
|
+
avatar_url: string | null;
|
|
42
|
+
bio: string | null;
|
|
43
|
+
role: string;
|
|
44
|
+
is_active: boolean;
|
|
45
|
+
created_at: string;
|
|
46
|
+
updated_at: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ApiResponse<T> {
|
|
50
|
+
success: boolean;
|
|
51
|
+
data?: T;
|
|
52
|
+
message?: string;
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException, status
|
|
2
|
+
from src.service.user_service import user_service
|
|
3
|
+
from src.types.user_type import CreateUserDto, UpdateUserDto, ApiResponse
|
|
4
|
+
from src.function.helper import validate_create_user_dto, validate_update_user_dto, validate_string
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix="/users", tags=["Users"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@router.get("/", response_model=ApiResponse)
|
|
10
|
+
async def get_all_users():
|
|
11
|
+
try:
|
|
12
|
+
users = await user_service.get_all_users()
|
|
13
|
+
return ApiResponse(
|
|
14
|
+
success=True,
|
|
15
|
+
data=users,
|
|
16
|
+
message="Users retrieved successfully",
|
|
17
|
+
)
|
|
18
|
+
except ValueError as e:
|
|
19
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
20
|
+
except Exception as e:
|
|
21
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.get("/{user_id}", response_model=ApiResponse)
|
|
25
|
+
async def get_user_by_id(user_id: int):
|
|
26
|
+
try:
|
|
27
|
+
if user_id <= 0:
|
|
28
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid user ID")
|
|
29
|
+
|
|
30
|
+
user = await user_service.get_user_by_id(user_id)
|
|
31
|
+
if not user:
|
|
32
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
|
33
|
+
|
|
34
|
+
return ApiResponse(
|
|
35
|
+
success=True,
|
|
36
|
+
data=user,
|
|
37
|
+
message="User retrieved successfully",
|
|
38
|
+
)
|
|
39
|
+
except HTTPException:
|
|
40
|
+
raise
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.get("/search/{keyword}", response_model=ApiResponse)
|
|
46
|
+
async def search_users(keyword: str):
|
|
47
|
+
try:
|
|
48
|
+
if not validate_string(keyword, 1, 100):
|
|
49
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Search keyword must be between 1 and 100 characters")
|
|
50
|
+
|
|
51
|
+
users = await user_service.search_users(keyword)
|
|
52
|
+
return ApiResponse(
|
|
53
|
+
success=True,
|
|
54
|
+
data=users,
|
|
55
|
+
message="Users retrieved successfully",
|
|
56
|
+
)
|
|
57
|
+
except HTTPException:
|
|
58
|
+
raise
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.post("/", response_model=ApiResponse, status_code=status.HTTP_201_CREATED)
|
|
64
|
+
async def create_user(data: dict):
|
|
65
|
+
try:
|
|
66
|
+
validated_data = validate_create_user_dto(data)
|
|
67
|
+
user = await user_service.create_user(validated_data)
|
|
68
|
+
return ApiResponse(
|
|
69
|
+
success=True,
|
|
70
|
+
data=user,
|
|
71
|
+
message="User created successfully",
|
|
72
|
+
)
|
|
73
|
+
except ValueError as e:
|
|
74
|
+
error_message = str(e)
|
|
75
|
+
if "already exists" in error_message or "already taken" in error_message:
|
|
76
|
+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=error_message)
|
|
77
|
+
else:
|
|
78
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_message)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.put("/{user_id}", response_model=ApiResponse)
|
|
84
|
+
async def update_user(user_id: int, data: dict):
|
|
85
|
+
try:
|
|
86
|
+
if user_id <= 0:
|
|
87
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid user ID")
|
|
88
|
+
|
|
89
|
+
validated_data = validate_update_user_dto(data)
|
|
90
|
+
user = await user_service.update_user(user_id, validated_data)
|
|
91
|
+
if not user:
|
|
92
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
|
93
|
+
|
|
94
|
+
return ApiResponse(
|
|
95
|
+
success=True,
|
|
96
|
+
data=user,
|
|
97
|
+
message="User updated successfully",
|
|
98
|
+
)
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
error_message = str(e)
|
|
101
|
+
if "already in use" in error_message or "already taken" in error_message:
|
|
102
|
+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=error_message)
|
|
103
|
+
elif "not found" in error_message:
|
|
104
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error_message)
|
|
105
|
+
else:
|
|
106
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_message)
|
|
107
|
+
except HTTPException:
|
|
108
|
+
raise
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@router.delete("/{user_id}", response_model=ApiResponse)
|
|
114
|
+
async def delete_user(user_id: int):
|
|
115
|
+
try:
|
|
116
|
+
if user_id <= 0:
|
|
117
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid user ID")
|
|
118
|
+
|
|
119
|
+
deleted = await user_service.delete_user(user_id)
|
|
120
|
+
if not deleted:
|
|
121
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
|
122
|
+
|
|
123
|
+
return ApiResponse(
|
|
124
|
+
success=True,
|
|
125
|
+
message="User deleted successfully",
|
|
126
|
+
)
|
|
127
|
+
except ValueError as e:
|
|
128
|
+
error_message = str(e)
|
|
129
|
+
if "not found" in error_message:
|
|
130
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error_message)
|
|
131
|
+
else:
|
|
132
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_message)
|
|
133
|
+
except HTTPException:
|
|
134
|
+
raise
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@router.post("/{user_id}/soft-delete", response_model=ApiResponse)
|
|
140
|
+
async def soft_delete_user(user_id: int):
|
|
141
|
+
try:
|
|
142
|
+
if user_id <= 0:
|
|
143
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid user ID")
|
|
144
|
+
|
|
145
|
+
user = await user_service.soft_delete_user(user_id)
|
|
146
|
+
if not user:
|
|
147
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
|
148
|
+
|
|
149
|
+
return ApiResponse(
|
|
150
|
+
success=True,
|
|
151
|
+
data=user,
|
|
152
|
+
message="User soft deleted successfully",
|
|
153
|
+
)
|
|
154
|
+
except ValueError as e:
|
|
155
|
+
error_message = str(e)
|
|
156
|
+
if "not found" in error_message:
|
|
157
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error_message)
|
|
158
|
+
else:
|
|
159
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_message)
|
|
160
|
+
except HTTPException:
|
|
161
|
+
raise
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
from src.types.user_type import CreateUserDto, UpdateUserDto
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_email(email: str) -> bool:
|
|
7
|
+
email_regex = r"^[^\s@]+@[^\s@]+\.[^\s@]+$"
|
|
8
|
+
return re.match(email_regex, email) is not None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_string(value: Any, min_length: int = 1, max_length: int = 255) -> bool:
|
|
12
|
+
return isinstance(value, str) and min_length <= len(value) <= max_length
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate_boolean(value: Any) -> bool:
|
|
16
|
+
return isinstance(value, bool)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_create_user_dto(data: dict) -> CreateUserDto:
|
|
20
|
+
errors = []
|
|
21
|
+
|
|
22
|
+
if not validate_email(data.get("email", "")):
|
|
23
|
+
errors.append("Invalid email format")
|
|
24
|
+
|
|
25
|
+
if not validate_string(data.get("username"), 3, 100):
|
|
26
|
+
errors.append("Username must be between 3 and 100 characters")
|
|
27
|
+
|
|
28
|
+
if not validate_string(data.get("full_name"), 1, 255):
|
|
29
|
+
errors.append("Full name must be between 1 and 255 characters")
|
|
30
|
+
|
|
31
|
+
if not validate_string(data.get("password"), 6, 255):
|
|
32
|
+
errors.append("Password must be at least 6 characters")
|
|
33
|
+
|
|
34
|
+
avatar_url = data.get("avatar_url")
|
|
35
|
+
if avatar_url is not None and not validate_string(avatar_url, 1, 1000):
|
|
36
|
+
errors.append("Avatar URL must be a valid string")
|
|
37
|
+
|
|
38
|
+
bio = data.get("bio")
|
|
39
|
+
if bio is not None and not validate_string(bio, 1, 1000):
|
|
40
|
+
errors.append("Bio must be a valid string")
|
|
41
|
+
|
|
42
|
+
role = data.get("role")
|
|
43
|
+
if role is not None and not validate_string(role, 1, 50):
|
|
44
|
+
errors.append("Role must be a valid string")
|
|
45
|
+
|
|
46
|
+
if errors:
|
|
47
|
+
raise ValueError(f"Validation error: {', '.join(errors)}")
|
|
48
|
+
|
|
49
|
+
return CreateUserDto(
|
|
50
|
+
email=data["email"],
|
|
51
|
+
username=data["username"],
|
|
52
|
+
full_name=data["full_name"],
|
|
53
|
+
password=data["password"],
|
|
54
|
+
avatar_url=avatar_url,
|
|
55
|
+
bio=bio,
|
|
56
|
+
role=role or "user",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def validate_update_user_dto(data: dict) -> UpdateUserDto:
|
|
61
|
+
errors = []
|
|
62
|
+
|
|
63
|
+
email = data.get("email")
|
|
64
|
+
if email is not None and not validate_email(email):
|
|
65
|
+
errors.append("Invalid email format")
|
|
66
|
+
|
|
67
|
+
username = data.get("username")
|
|
68
|
+
if username is not None and not validate_string(username, 3, 100):
|
|
69
|
+
errors.append("Username must be between 3 and 100 characters")
|
|
70
|
+
|
|
71
|
+
full_name = data.get("full_name")
|
|
72
|
+
if full_name is not None and not validate_string(full_name, 1, 255):
|
|
73
|
+
errors.append("Full name must be between 1 and 255 characters")
|
|
74
|
+
|
|
75
|
+
password = data.get("password")
|
|
76
|
+
if password is not None and not validate_string(password, 6, 255):
|
|
77
|
+
errors.append("Password must be at least 6 characters")
|
|
78
|
+
|
|
79
|
+
avatar_url = data.get("avatar_url")
|
|
80
|
+
if avatar_url is not None and not validate_string(avatar_url, 1, 1000) and avatar_url is not None:
|
|
81
|
+
errors.append("Avatar URL must be a valid string or None")
|
|
82
|
+
|
|
83
|
+
bio = data.get("bio")
|
|
84
|
+
if bio is not None and not validate_string(bio, 1, 1000) and bio is not None:
|
|
85
|
+
errors.append("Bio must be a valid string or None")
|
|
86
|
+
|
|
87
|
+
role = data.get("role")
|
|
88
|
+
if role is not None and not validate_string(role, 1, 50):
|
|
89
|
+
errors.append("Role must be a valid string")
|
|
90
|
+
|
|
91
|
+
is_active = data.get("is_active")
|
|
92
|
+
if is_active is not None and not validate_boolean(is_active):
|
|
93
|
+
errors.append("is_active must be a boolean")
|
|
94
|
+
|
|
95
|
+
if errors:
|
|
96
|
+
raise ValueError(f"Validation error: {', '.join(errors)}")
|
|
97
|
+
|
|
98
|
+
return UpdateUserDto(
|
|
99
|
+
email=email,
|
|
100
|
+
username=username,
|
|
101
|
+
full_name=full_name,
|
|
102
|
+
password=password,
|
|
103
|
+
avatar_url=avatar_url,
|
|
104
|
+
bio=bio,
|
|
105
|
+
role=role,
|
|
106
|
+
is_active=is_active,
|
|
107
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import src.sql.user_sql as UserSQL
|
|
2
|
+
from src.types.user_type import User, CreateUserDto, UpdateUserDto, UserResponse
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class UserService:
|
|
6
|
+
async def get_all_users(self):
|
|
7
|
+
users = await UserSQL.get_users()
|
|
8
|
+
return [self.map_to_response(user) for user in users]
|
|
9
|
+
|
|
10
|
+
async def get_user_by_id(self, user_id: int):
|
|
11
|
+
user = await UserSQL.get_user_by_id(user_id)
|
|
12
|
+
if not user:
|
|
13
|
+
return None
|
|
14
|
+
return self.map_to_response(user)
|
|
15
|
+
|
|
16
|
+
async def get_user_by_email(self, email: str):
|
|
17
|
+
return await UserSQL.get_user_by_email(email)
|
|
18
|
+
|
|
19
|
+
async def get_user_by_username(self, username: str):
|
|
20
|
+
return await UserSQL.get_user_by_username(username)
|
|
21
|
+
|
|
22
|
+
async def create_user(self, data: CreateUserDto):
|
|
23
|
+
existing_user = await UserSQL.get_user_by_email(data.email)
|
|
24
|
+
if existing_user:
|
|
25
|
+
raise ValueError("User with this email already exists")
|
|
26
|
+
|
|
27
|
+
existing_username = await UserSQL.get_user_by_username(data.username)
|
|
28
|
+
if existing_username:
|
|
29
|
+
raise ValueError("Username already taken")
|
|
30
|
+
|
|
31
|
+
user = await UserSQL.create_user(data)
|
|
32
|
+
return self.map_to_response(user)
|
|
33
|
+
|
|
34
|
+
async def update_user(self, user_id: int, data: UpdateUserDto):
|
|
35
|
+
existing_user = await UserSQL.get_user_by_id(user_id)
|
|
36
|
+
if not existing_user:
|
|
37
|
+
raise ValueError("User not found")
|
|
38
|
+
|
|
39
|
+
if data.email:
|
|
40
|
+
email_user = await UserSQL.get_user_by_email(data.email)
|
|
41
|
+
if email_user and email_user["id"] != user_id:
|
|
42
|
+
raise ValueError("Email already in use")
|
|
43
|
+
|
|
44
|
+
if data.username:
|
|
45
|
+
username_user = await UserSQL.get_user_by_username(data.username)
|
|
46
|
+
if username_user and username_user["id"] != user_id:
|
|
47
|
+
raise ValueError("Username already taken")
|
|
48
|
+
|
|
49
|
+
filtered_data = {k: v for k, v in data.model_dump().items() if v is not None}
|
|
50
|
+
if not filtered_data:
|
|
51
|
+
return await self.get_user_by_id(user_id)
|
|
52
|
+
|
|
53
|
+
user = await UserSQL.update_user(user_id, UpdateUserDto(**filtered_data))
|
|
54
|
+
if not user:
|
|
55
|
+
return None
|
|
56
|
+
return self.map_to_response(user)
|
|
57
|
+
|
|
58
|
+
async def delete_user(self, user_id: int) -> bool:
|
|
59
|
+
existing_user = await UserSQL.get_user_by_id(user_id)
|
|
60
|
+
if not existing_user:
|
|
61
|
+
raise ValueError("User not found")
|
|
62
|
+
|
|
63
|
+
return await UserSQL.delete_user(user_id)
|
|
64
|
+
|
|
65
|
+
async def soft_delete_user(self, user_id: int):
|
|
66
|
+
existing_user = await UserSQL.get_user_by_id(user_id)
|
|
67
|
+
if not existing_user:
|
|
68
|
+
raise ValueError("User not found")
|
|
69
|
+
|
|
70
|
+
user = await UserSQL.soft_delete_user(user_id)
|
|
71
|
+
if not user:
|
|
72
|
+
return None
|
|
73
|
+
return self.map_to_response(user)
|
|
74
|
+
|
|
75
|
+
async def search_users(self, keyword: str):
|
|
76
|
+
users = await UserSQL.search_users(keyword)
|
|
77
|
+
return [self.map_to_response(user) for user in users]
|
|
78
|
+
|
|
79
|
+
def map_to_response(self, user: dict) -> dict:
|
|
80
|
+
return {
|
|
81
|
+
"id": user["id"],
|
|
82
|
+
"email": user["email"],
|
|
83
|
+
"username": user["username"],
|
|
84
|
+
"full_name": user["full_name"],
|
|
85
|
+
"avatar_url": user["avatar_url"],
|
|
86
|
+
"bio": user["bio"],
|
|
87
|
+
"role": user["role"],
|
|
88
|
+
"is_active": user["is_active"],
|
|
89
|
+
"created_at": user["created_at"],
|
|
90
|
+
"updated_at": user["updated_at"],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
user_service = UserService()
|