Skip to content

Commit 5829ea6

Browse files
committed
fix(api): fix swagger docs to properly generate api-client
fix(web): admin scope the settings pages
1 parent 073c56b commit 5829ea6

21 files changed

Lines changed: 1635 additions & 349 deletions

File tree

apps/api/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,10 @@ app.use("/api/", rateLimiters.standard);
9494
// CORS configuration
9595
app.use(
9696
cors({
97-
origin: env.CORS_ORIGIN,
97+
origin: env.CORS_ORIGIN.split(",").map((origin) => origin.trim()),
9898
credentials: true,
99+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
100+
allowedHeaders: ["Content-Type", "Authorization", "x-csrf-token"],
99101
})
100102
);
101103

apps/api/src/lib/auth/user.service.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ export class UserService {
108108
);
109109
}
110110

111+
// Check if this is the first user - make them admin
112+
const userCount = await prisma.user.count();
113+
const finalRole = userCount === 0 ? "ADMIN" : role;
114+
111115
// Validate authentication method
112116
if (!isPasswordless && !password && !pin) {
113117
throw new BadRequestError(
@@ -143,12 +147,14 @@ export class UserService {
143147
passwordHash,
144148
pinHash,
145149
isPasswordless,
146-
role,
150+
role: finalRole,
147151
lastLoginAt: new Date(),
148152
},
149153
});
150154

151-
logger.info(`User created: ${user.username} (${user.id})`);
155+
logger.info(
156+
`User created: ${user.username} (${user.id}) - Role: ${finalRole}`
157+
);
152158

153159
// Generate tokens
154160
const accessToken = generateAccessToken({

apps/api/src/routes/auth/auth.module.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const router: Router = Router();
3333

3434
/**
3535
* @swagger
36-
* /auth/register:
36+
* /api/v1/auth/register:
3737
* post:
3838
* tags: [Authentication]
3939
* summary: Register a new user
@@ -80,7 +80,7 @@ router.post(
8080

8181
/**
8282
* @swagger
83-
* /auth/login:
83+
* /api/v1/auth/login:
8484
* post:
8585
* tags: [Authentication]
8686
* summary: Login with username and password or PIN
@@ -111,7 +111,7 @@ router.post(
111111

112112
/**
113113
* @swagger
114-
* /auth/login/passwordless:
114+
* /api/v1/auth/login/passwordless:
115115
* post:
116116
* tags: [Authentication]
117117
* summary: Login without password (passwordless accounts only)
@@ -138,7 +138,7 @@ router.post(
138138

139139
/**
140140
* @swagger
141-
* /auth/refresh:
141+
* /api/v1/auth/refresh:
142142
* post:
143143
* tags: [Authentication]
144144
* summary: Refresh access token
@@ -169,7 +169,7 @@ router.post(
169169

170170
/**
171171
* @swagger
172-
* /auth/logout:
172+
* /api/v1/auth/logout:
173173
* post:
174174
* tags: [Authentication]
175175
* summary: Logout current session
@@ -199,7 +199,7 @@ router.post(
199199

200200
/**
201201
* @swagger
202-
* /auth/logout-all:
202+
* /api/v1/auth/logout-all:
203203
* post:
204204
* tags: [Authentication]
205205
* summary: Logout all sessions
@@ -218,7 +218,7 @@ router.post(
218218

219219
/**
220220
* @swagger
221-
* /auth/me:
221+
* /api/v1/auth/me:
222222
* get:
223223
* tags: [Authentication]
224224
* summary: Get current user information
@@ -237,7 +237,7 @@ router.get(
237237

238238
/**
239239
* @swagger
240-
* /auth/me:
240+
* /api/v1/auth/me:
241241
* put:
242242
* tags: [Authentication]
243243
* summary: Update current user
@@ -257,7 +257,7 @@ router.put(
257257

258258
/**
259259
* @swagger
260-
* /auth/change-password:
260+
* /api/v1/auth/change-password:
261261
* post:
262262
* tags: [Authentication]
263263
* summary: Change password
@@ -277,7 +277,7 @@ router.post(
277277

278278
/**
279279
* @swagger
280-
* /auth/change-pin:
280+
* /api/v1/auth/change-pin:
281281
* post:
282282
* tags: [Authentication]
283283
* summary: Change PIN
@@ -301,7 +301,7 @@ router.post(
301301

302302
/**
303303
* @swagger
304-
* /auth/sessions:
304+
* /api/v1/auth/sessions:
305305
* get:
306306
* tags: [Authentication]
307307
* summary: Get all active sessions
@@ -320,7 +320,7 @@ router.get(
320320

321321
/**
322322
* @swagger
323-
* /auth/sessions/{sessionId}:
323+
* /api/v1/auth/sessions/{sessionId}:
324324
* delete:
325325
* tags: [Authentication]
326326
* summary: Revoke a specific session
@@ -349,7 +349,7 @@ router.delete(
349349

350350
/**
351351
* @swagger
352-
* /auth/api-keys:
352+
* /api/v1/auth/api-keys:
353353
* get:
354354
* tags: [API Keys]
355355
* summary: List all API keys
@@ -367,7 +367,7 @@ router.get(
367367

368368
/**
369369
* @swagger
370-
* /auth/api-keys:
370+
* /api/v1/auth/api-keys:
371371
* post:
372372
* tags: [API Keys]
373373
* summary: Create a new API key
@@ -386,7 +386,7 @@ router.post(
386386

387387
/**
388388
* @swagger
389-
* /auth/api-keys/{keyId}:
389+
* /api/v1/auth/api-keys/{keyId}:
390390
* get:
391391
* tags: [API Keys]
392392
* summary: Get a specific API key
@@ -410,7 +410,7 @@ router.get(
410410

411411
/**
412412
* @swagger
413-
* /auth/api-keys/{keyId}:
413+
* /api/v1/auth/api-keys/{keyId}:
414414
* put:
415415
* tags: [API Keys]
416416
* summary: Update an API key
@@ -435,7 +435,7 @@ router.put(
435435

436436
/**
437437
* @swagger
438-
* /auth/api-keys/{keyId}:
438+
* /api/v1/auth/api-keys/{keyId}:
439439
* delete:
440440
* tags: [API Keys]
441441
* summary: Delete an API key
@@ -459,7 +459,7 @@ router.delete(
459459

460460
/**
461461
* @swagger
462-
* /auth/api-keys/{keyId}/revoke:
462+
* /api/v1/auth/api-keys/{keyId}/revoke:
463463
* post:
464464
* tags: [API Keys]
465465
* summary: Revoke an API key
@@ -487,7 +487,7 @@ router.post(
487487

488488
/**
489489
* @swagger
490-
* /users:
490+
* /api/v1/users:
491491
* get:
492492
* tags: [Users]
493493
* summary: List all users (admin only)
@@ -507,7 +507,7 @@ router.get(
507507

508508
/**
509509
* @swagger
510-
* /users/{userId}:
510+
* /api/v1/users/{userId}:
511511
* get:
512512
* tags: [Users]
513513
* summary: Get user by ID (admin only)
@@ -533,7 +533,7 @@ router.get(
533533

534534
/**
535535
* @swagger
536-
* /users/{userId}:
536+
* /api/v1/users/{userId}:
537537
* put:
538538
* tags: [Users]
539539
* summary: Update user (admin only)
@@ -560,7 +560,7 @@ router.put(
560560

561561
/**
562562
* @swagger
563-
* /users/{userId}:
563+
* /api/v1/users/{userId}:
564564
* delete:
565565
* tags: [Users]
566566
* summary: Delete user (admin only)

apps/api/src/routes/comics/comics.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ router.get("/", (req, res, next) => {
121121

122122
/**
123123
* @openapi
124-
* /api/comics/{id}:
124+
* /api/v1/comics/{id}:
125125
* get:
126126
* summary: Get comic by ID
127127
* description: Retrieve a specific comic by its ID

apps/web/src/components/ui/header/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "./variants";
1818
import Logo from "../logo";
1919
import ModeDialog from "../mode-dialog";
20+
import UserMenu from "./user-menu";
2021
import { useSearch } from "@/lib/hooks";
2122
import type { GetApiV1SearchParams } from "@dester/api-client";
2223
import SearchResults from "./search-results";
@@ -222,6 +223,7 @@ const Header = () => {
222223
isDialogOpen={isDialogOpen}
223224
setIsDialogOpen={setIsDialogOpen}
224225
/>
226+
<UserMenu />
225227
</nav>
226228
</div>
227229
);
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { useState } from "react";
2+
import { useNavigate } from "@tanstack/react-router";
3+
import { useAuth } from "@/hooks/useAuth";
4+
import { Button } from "../button";
5+
import {
6+
User,
7+
Settings,
8+
LogOut,
9+
ChevronDown,
10+
Shield,
11+
LogIn,
12+
} from "lucide-react";
13+
import { AnimatePresence, motion } from "motion/react";
14+
15+
export default function UserMenu() {
16+
const { user, isAuthenticated, logout } = useAuth();
17+
const navigate = useNavigate();
18+
const [isOpen, setIsOpen] = useState(false);
19+
20+
const handleLogout = async () => {
21+
await logout();
22+
navigate({ to: "/login" });
23+
};
24+
25+
if (!isAuthenticated) {
26+
return (
27+
<Button
28+
onClick={() => navigate({ to: "/login" })}
29+
variant="ghost"
30+
className="flex items-center gap-2"
31+
>
32+
<LogIn className="w-4 h-4" />
33+
<span>Sign In</span>
34+
</Button>
35+
);
36+
}
37+
38+
return (
39+
<div className="relative">
40+
<Button
41+
onClick={() => setIsOpen(!isOpen)}
42+
variant="ghost"
43+
className="flex items-center gap-2 min-w-[120px] justify-between"
44+
>
45+
<div className="flex items-center gap-2">
46+
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center">
47+
{user?.role === "ADMIN" ? (
48+
<Shield className="w-4 h-4 text-white" />
49+
) : (
50+
<User className="w-4 h-4 text-white" />
51+
)}
52+
</div>
53+
<span className="text-sm font-medium">{user?.username}</span>
54+
</div>
55+
<ChevronDown
56+
className={`w-4 h-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
57+
/>
58+
</Button>
59+
60+
<AnimatePresence>
61+
{isOpen && (
62+
<>
63+
{/* Backdrop to close menu */}
64+
<div
65+
className="fixed inset-0 z-40"
66+
onClick={() => setIsOpen(false)}
67+
/>
68+
69+
{/* Dropdown Menu */}
70+
<motion.div
71+
initial={{ opacity: 0, y: -10 }}
72+
animate={{ opacity: 1, y: 0 }}
73+
exit={{ opacity: 0, y: -10 }}
74+
transition={{ duration: 0.15 }}
75+
className="absolute right-0 mt-2 w-56 bg-white/10 backdrop-blur-xl border border-white/20 rounded-xl shadow-2xl z-50 overflow-hidden"
76+
>
77+
{/* User Info */}
78+
<div className="px-4 py-3 border-b border-white/10">
79+
<p className="text-sm font-medium text-white">
80+
{user?.username}
81+
</p>
82+
{user?.email && (
83+
<p className="text-xs text-white/60 mt-0.5">{user.email}</p>
84+
)}
85+
<div className="mt-2">
86+
<span
87+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
88+
user?.role === "ADMIN"
89+
? "bg-purple-500/20 text-purple-300"
90+
: "bg-blue-500/20 text-blue-300"
91+
}`}
92+
>
93+
{user?.role === "ADMIN" && <Shield className="w-3 h-3" />}
94+
{user?.role === "ADMIN" ? "Admin" : "User"}
95+
</span>
96+
</div>
97+
</div>
98+
99+
{/* Menu Items */}
100+
<div className="py-1">
101+
<button
102+
onClick={() => {
103+
navigate({ to: "/settings" });
104+
setIsOpen(false);
105+
}}
106+
className="w-full px-4 py-2.5 text-left text-sm text-white hover:bg-white/10 transition-colors flex items-center gap-3"
107+
>
108+
<Settings className="w-4 h-4" />
109+
Settings
110+
</button>
111+
112+
<button
113+
onClick={() => {
114+
handleLogout();
115+
setIsOpen(false);
116+
}}
117+
className="w-full px-4 py-2.5 text-left text-sm text-red-400 hover:bg-red-500/10 transition-colors flex items-center gap-3"
118+
>
119+
<LogOut className="w-4 h-4" />
120+
Sign Out
121+
</button>
122+
</div>
123+
</motion.div>
124+
</>
125+
)}
126+
</AnimatePresence>
127+
</div>
128+
);
129+
}

0 commit comments

Comments
 (0)