Skip to content

Commit c94dd71

Browse files
committed
Add admin user
Signed-off-by: SeeuSim <[email protected]>
1 parent 0f7d97c commit c94dd71

File tree

13 files changed

+221
-20
lines changed

13 files changed

+221
-20
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "users" ADD COLUMN "is_admin" boolean DEFAULT false;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
{
2+
"id": "5293f5bb-f4d5-43a4-b2bf-f6ebb241dde1",
3+
"prevId": "f01e3d91-1038-48b8-a073-a6a9a8a308fd",
4+
"version": "7",
5+
"dialect": "postgresql",
6+
"tables": {
7+
"public.admin": {
8+
"name": "admin",
9+
"schema": "",
10+
"columns": {
11+
"id": {
12+
"name": "id",
13+
"type": "uuid",
14+
"primaryKey": true,
15+
"notNull": true,
16+
"default": "gen_random_uuid()"
17+
},
18+
"created_at": {
19+
"name": "created_at",
20+
"type": "timestamp",
21+
"primaryKey": false,
22+
"notNull": false,
23+
"default": "now()"
24+
},
25+
"action": {
26+
"name": "action",
27+
"type": "action",
28+
"typeSchema": "public",
29+
"primaryKey": false,
30+
"notNull": true
31+
}
32+
},
33+
"indexes": {},
34+
"foreignKeys": {},
35+
"compositePrimaryKeys": {},
36+
"uniqueConstraints": {}
37+
},
38+
"public.users": {
39+
"name": "users",
40+
"schema": "",
41+
"columns": {
42+
"id": {
43+
"name": "id",
44+
"type": "uuid",
45+
"primaryKey": true,
46+
"notNull": true,
47+
"default": "gen_random_uuid()"
48+
},
49+
"email": {
50+
"name": "email",
51+
"type": "varchar(255)",
52+
"primaryKey": false,
53+
"notNull": true
54+
},
55+
"username": {
56+
"name": "username",
57+
"type": "varchar(255)",
58+
"primaryKey": false,
59+
"notNull": true
60+
},
61+
"first_name": {
62+
"name": "first_name",
63+
"type": "varchar(255)",
64+
"primaryKey": false,
65+
"notNull": true
66+
},
67+
"last_name": {
68+
"name": "last_name",
69+
"type": "varchar(255)",
70+
"primaryKey": false,
71+
"notNull": true
72+
},
73+
"password": {
74+
"name": "password",
75+
"type": "varchar(255)",
76+
"primaryKey": false,
77+
"notNull": true
78+
},
79+
"failed_attempts": {
80+
"name": "failed_attempts",
81+
"type": "smallint",
82+
"primaryKey": false,
83+
"notNull": false,
84+
"default": 0
85+
},
86+
"unlock_time": {
87+
"name": "unlock_time",
88+
"type": "timestamp (6) with time zone",
89+
"primaryKey": false,
90+
"notNull": false
91+
},
92+
"attempted_questions": {
93+
"name": "attempted_questions",
94+
"type": "integer[]",
95+
"primaryKey": false,
96+
"notNull": false
97+
},
98+
"is_admin": {
99+
"name": "is_admin",
100+
"type": "boolean",
101+
"primaryKey": false,
102+
"notNull": false,
103+
"default": false
104+
}
105+
},
106+
"indexes": {},
107+
"foreignKeys": {},
108+
"compositePrimaryKeys": {},
109+
"uniqueConstraints": {
110+
"users_email_unique": {
111+
"name": "users_email_unique",
112+
"nullsNotDistinct": false,
113+
"columns": [
114+
"email"
115+
]
116+
},
117+
"users_username_unique": {
118+
"name": "users_username_unique",
119+
"nullsNotDistinct": false,
120+
"columns": [
121+
"username"
122+
]
123+
}
124+
}
125+
}
126+
},
127+
"enums": {
128+
"public.action": {
129+
"name": "action",
130+
"schema": "public",
131+
"values": [
132+
"SEED"
133+
]
134+
}
135+
},
136+
"schemas": {},
137+
"sequences": {},
138+
"_meta": {
139+
"columns": {},
140+
"schemas": {},
141+
"tables": {}
142+
}
143+
}

backend/user/drizzle/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
"when": 1728143079049,
99
"tag": "0000_initial_schema",
1010
"breakpoints": true
11+
},
12+
{
13+
"idx": 1,
14+
"version": "7",
15+
"when": 1730968227580,
16+
"tag": "0001_add_admin_user",
17+
"breakpoints": true
1118
}
1219
]
1320
}

backend/user/src/controllers/auth-check/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const checkIsAuthed: IRouteHandler = async (req, res) => {
1616
'[/auth-check/check-is-authed]: Expires At ' + new Date(expireTimeInMillis).toLocaleString()
1717
);
1818
const user = await db
19-
.select({ name: users.username })
19+
.select({ name: users.username, isAdmin: users.isAdmin, email: users.email })
2020
.from(users)
2121
.where(eq(users.id, decoded.id))
2222
.limit(1);
@@ -25,6 +25,8 @@ export const checkIsAuthed: IRouteHandler = async (req, res) => {
2525
expiresAt: expireTimeInMillis,
2626
userId: decoded.id,
2727
username: user.length > 0 ? user[0].name : undefined,
28+
email: user.length > 0 ? user[0].email : undefined,
29+
isAdmin: user.length > 0 ? user[0].isAdmin : undefined,
2830
});
2931
}
3032

backend/user/src/lib/db/schema.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { integer, pgEnum, pgTable, smallint, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
1+
import {
2+
boolean,
3+
integer,
4+
pgEnum,
5+
pgTable,
6+
smallint,
7+
timestamp,
8+
uuid,
9+
varchar,
10+
} from 'drizzle-orm/pg-core';
211

312
// Define the user table
413
export const users = pgTable('users', {
@@ -11,6 +20,7 @@ export const users = pgTable('users', {
1120
failedAttempts: smallint('failed_attempts').default(0), // Failed counts
1221
unlockTime: timestamp('unlock_time', { precision: 6, withTimezone: true }), // If failed counts > limit, block all attempts until this time.
1322
attemptedQuestions: integer('attempted_questions').array(),
23+
isAdmin: boolean('is_admin').default(false),
1424
});
1525

1626
export const actionEnum = pgEnum('action', ['SEED']);

backend/user/src/lib/db/seed.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { eq } from 'drizzle-orm';
1+
import { eq, InferInsertModel } from 'drizzle-orm';
22

33
import { generatePasswordHash } from '@/lib/passwords';
44

55
import { admin as adminTable, db, users as usersTable } from '.';
66

7-
const TEST_USER_CREDENTIALS = [
7+
const TEST_USER_CREDENTIALS: Array<InferInsertModel<typeof usersTable>> = [
88
{
99
username: 'testuser01',
1010
@@ -19,6 +19,14 @@ const TEST_USER_CREDENTIALS = [
1919
lastName: 'user02',
2020
password: '123456789', // For local testing purposes
2121
},
22+
{
23+
username: 'adminuser01',
24+
25+
firstName: 'admin',
26+
lastName: 'user01',
27+
password: 'IamPeerprepAdmin!9',
28+
isAdmin: true,
29+
},
2230
];
2331

2432
const main = async () => {

frontend/src/components/blocks/nav-bar.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ import { UserDropdown } from '@/components/common/user-dropdown';
99
import { Button } from '@/components/ui/button';
1010
import { useRouterLocation } from '@/lib/hooks';
1111
import { ROUTES } from '@/lib/routes';
12+
import { useAuthedRoute } from '@/stores/auth-store';
13+
14+
import { Badge } from '../ui/badge';
1215

1316
const NavBar = observer(() => {
17+
const data = useAuthedRoute();
1418
const { isLogin, isUnauthedRoute } = useRouterLocation();
1519

1620
return (
@@ -41,7 +45,14 @@ const NavBar = observer(() => {
4145
</Button>
4246
)
4347
) : (
44-
<UserDropdown />
48+
<>
49+
{data?.isAdmin && (
50+
<Badge className='uppercase' variant='easy'>
51+
<span>Admin</span>
52+
</Badge>
53+
)}
54+
<UserDropdown />
55+
</>
4556
)}
4657
</div>
4758
</nav>
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { Outlet } from 'react-router-dom';
22

3-
import NavBar from './nav-bar';
4-
53
export function RootLayout() {
64
return (
75
<div className='text-text bg-background flex min-h-screen flex-col overscroll-contain'>
8-
<NavBar />
9-
<main className='flex flex-1'>
10-
<Outlet />
11-
</main>
6+
<Outlet />
127
</div>
138
);
149
}

frontend/src/components/blocks/route-guard.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { checkIsAuthed } from '@/services/user-service';
1414
import { AuthStoreProvider } from '@/stores/auth-store';
1515

1616
import { Loading } from './loading';
17+
import NavBar from './nav-bar';
1718

1819
export const loader =
1920
(queryClient: QueryClient) =>
@@ -61,9 +62,12 @@ export const RouteGuard = () => {
6162
value={{
6263
userId: authedPayload.userId ?? '',
6364
username: authedPayload.username ?? '',
65+
email: authedPayload.email ?? '',
66+
isAdmin: authedPayload.isAdmin ?? undefined,
6467
}}
6568
>
66-
{isLoading ? <Loading /> : <Outlet />}
69+
<NavBar />
70+
<main className='flex flex-1'>{isLoading ? <Loading /> : <Outlet />}</main>
6771
</AuthStoreProvider>
6872
);
6973
}}

frontend/src/routes/login/login-form.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ export const LoginForm = () => {
4646
<FormItem>
4747
<FormLabel>Password</FormLabel>
4848
<FormControl>
49-
<Input type='password' disabled={isPending} placeholder='••••••••' {...field} />
49+
<Input
50+
type='password'
51+
autoComplete='new-password'
52+
disabled={isPending}
53+
placeholder='••••••••'
54+
{...field}
55+
/>
5056
</FormControl>
5157
<FormMessage />
5258
<div className='flex w-full'>

0 commit comments

Comments
 (0)