Skip to content

Commit bd1c8cc

Browse files
committed
feat(user): add sign-up page, simplify user services, and integrate auth hook
1 parent 9984efa commit bd1c8cc

File tree

24 files changed

+642
-200
lines changed

24 files changed

+642
-200
lines changed

README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,16 @@ This project uses:
150150
> **⚠️ Warning:**
151151
> If you encounter any difficulties or something doesn't go as planned, read [this file](scripts/README.md) to execute it manually.
152152

153-
2. Create a user; open a browser to [http://localhost:5555](http://localhost:5555) and fill out Users table with fields:
154-
155-
- `name`
156-
- `surname`
157-
- `email`
158-
- `password`, the password must first be encrypted using [SHA-256](https://codebeautify.org/sha256-hash-generator), and then the resulting hash should be encrypted using [BCrypt](https://bcrypt-generator.com/) with 12 rounds (store this in password field).
153+
2. Create a user; open a browser to [http://localhost:3000/sign-up](http://localhost:3000/sign-up) and sign up.
159154

160155
3. _Optionally_, you can run the following exactly script to generate and fill database with fake data:
161156

162157
```bash
163158
npm run setup:data --user=<user_id_created_before>
164159
```
165160

161+
> You can find user ID on the [personal information page](http://localhost:3000/account/profile)
162+
166163
### Build and run
167164

168165
1. Build project

apps/api/src/controllers/auth.controller.ts

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,101 @@
11
import { Request, Response } from 'express'
22
import prisma from '@poveroh/prisma'
3-
import jwt from 'jsonwebtoken'
43
import bcrypt from 'bcryptjs'
5-
import { UAParser } from 'ua-parser-js'
6-
import { config } from '../utils/environment'
4+
import { AuthHelper } from '../helpers/auth.helper'
5+
import { IUserToSave } from '@poveroh/types/dist'
76

87
export class AuthController {
98
static async signIn(req: Request, res: Response) {
109
try {
1110
const { email, password } = req.body
1211

1312
if (!email || !password) {
14-
res.status(400).json({
15-
message: 'Email and password are required'
16-
})
13+
res.status(400).json({ message: 'Email and password are required' })
1714
return
1815
}
1916

20-
const user = await prisma.users.findUnique({
21-
where: {
22-
email: email
23-
}
24-
})
25-
17+
const user = await prisma.users.findUnique({ where: { email } })
2618
if (!user || !(await bcrypt.compare(password, user.password))) {
2719
res.status(401).json({ message: 'Invalid credentials' })
2820
return
2921
}
3022

31-
const parser = new UAParser()
32-
const agent = parser.setUA('Mozilla/5.0 ...').getResult()
33-
34-
const browser = `${agent.browser.name} ${agent.browser.major}`
35-
const os = `${agent.os.name} ${agent.os.version}`
23+
const { browser, os } = AuthHelper.getDeviceInfo(req.headers['user-agent'])
3624

3725
await prisma.users_login.create({
3826
data: {
3927
device: os,
4028
browser: browser,
41-
ip: '-',
29+
ip: req.ip || '-',
4230
location: '-',
4331
user_id: user.id
4432
}
4533
})
4634

47-
const token: string = jwt.sign({ id: user.id, email: user.email }, config.JWT_SECRET || '-', {
48-
expiresIn: '24h'
35+
const token = AuthHelper.generateToken(user)
36+
res.cookie('token', token, { maxAge: 24 * 60 * 60 * 1000 })
37+
38+
res.status(200).json({ success: true })
39+
} catch (error: any) {
40+
console.error(error)
41+
res.status(500).json({
42+
message: 'An error occurred during login',
43+
error: error.message
4944
})
45+
}
46+
}
47+
48+
static async signUp(req: Request, res: Response) {
49+
try {
50+
const user: IUserToSave = req.body
5051

52+
if (!user.email || !user.password || !user.name) {
53+
res.status(400).json({ message: 'Name, email, and password are required' })
54+
return
55+
}
56+
57+
const existingUser = await prisma.users.findUnique({ where: { email: user.email } })
58+
if (existingUser) {
59+
res.status(409).json({ message: 'Email already in use' })
60+
return
61+
}
62+
63+
const hashedPassword = await bcrypt.hash(user.password, 12)
64+
65+
const newUser = await prisma.users.create({
66+
data: {
67+
...user,
68+
password: hashedPassword
69+
},
70+
select: {
71+
id: true,
72+
name: true,
73+
surname: true,
74+
email: true,
75+
created_at: true
76+
}
77+
})
78+
79+
const { browser, os } = AuthHelper.getDeviceInfo(req.headers['user-agent'])
80+
81+
await prisma.users_login.create({
82+
data: {
83+
device: os,
84+
browser: browser,
85+
ip: req.ip || '-',
86+
location: '-',
87+
user_id: newUser.id
88+
}
89+
})
90+
91+
const token = AuthHelper.generateToken(newUser)
5192
res.cookie('token', token, { maxAge: 24 * 60 * 60 * 1000 })
5293

53-
res.status(200).json(true)
94+
res.status(200).json(newUser)
5495
} catch (error: any) {
55-
console.log(error)
96+
console.error(error)
5697
res.status(500).json({
57-
message: 'An error occurred during login',
98+
message: 'An error occurred during registration',
5899
error: error.message
59100
})
60101
}

apps/api/src/controllers/user.controller.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ export class UserController {
2727
}
2828
}
2929

30+
static async add(req: Request, res: Response) {
31+
try {
32+
const user = await prisma.users.findUnique({
33+
where: { email: req.user.email },
34+
select: {
35+
id: true,
36+
name: true,
37+
surname: true,
38+
email: true,
39+
created_at: true
40+
}
41+
})
42+
43+
if (!user) {
44+
res.status(404).json({ message: 'User not found' })
45+
return
46+
}
47+
48+
res.status(200).json(user)
49+
} catch (error) {
50+
res.status(500).json({ message: 'An error occurred', error })
51+
}
52+
}
53+
3054
static async save(req: Request, res: Response) {
3155
try {
3256
const user = await prisma.users.findUnique({
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { UAParser } from 'ua-parser-js'
2+
import { config } from '../utils/environment'
3+
import jwt from 'jsonwebtoken'
4+
5+
export const AuthHelper = {
6+
generateToken(user: { id: string; email: string }) {
7+
return jwt.sign({ id: user.id, email: user.email }, config.JWT_SECRET || '-', {
8+
expiresIn: '24h'
9+
})
10+
},
11+
12+
getDeviceInfo(userAgent?: string) {
13+
const parser = new UAParser()
14+
const agent = parser.setUA(userAgent || '').getResult()
15+
16+
return {
17+
browser: `${agent.browser.name || 'Unknown'} ${agent.browser.major || ''}`.trim(),
18+
os: `${agent.os.name || 'Unknown'} ${agent.os.version || ''}`.trim()
19+
}
20+
}
21+
}

apps/api/src/routes/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ import { AuthController } from '../controllers/auth.controller'
44
const router: Router = Router()
55

66
router.post('/login', AuthController.signIn)
7+
router.post('/sign-up', AuthController.signUp)
78

89
export default router

apps/api/src/routes/user.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const router: Router = Router()
66

77
router.post('/me', AuthMiddleware.isAuthenticated, UserController.me)
88
router.post('/save', AuthMiddleware.isAuthenticated, UserController.save)
9+
router.post('/add', AuthMiddleware.isAuthenticated, UserController.add)
910
router.post('/set-password', AuthMiddleware.isAuthenticated, UserController.updatePassword)
1011

1112
export default router

apps/app/app/(admin)/account/profile/view.tsx

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import { useEffect, useState } from 'react'
44
import { useForm } from 'react-hook-form'
55
import * as z from 'zod'
66
import { zodResolver } from '@hookform/resolvers/zod'
7-
import { isEqual } from 'lodash'
8-
import { useRouter } from 'next/navigation'
97
import { useTranslations } from 'next-intl'
108

9+
import { CopyableInput } from '@poveroh/ui/components/input-copyable'
1110
import { Button } from '@poveroh/ui/components/button'
1211
import {
1312
Breadcrumb,
@@ -27,23 +26,19 @@ import {
2726
FormMessage
2827
} from '@poveroh/ui/components/form'
2928
import { Input } from '@poveroh/ui/components/input'
30-
import { toast } from '@poveroh/ui/components/sonner'
3129

3230
import { Loader2 } from 'lucide-react'
3331

34-
import { UserService } from '@/services/user.service'
3532
import Box from '@/components/box/boxWrapper'
3633

3734
import { IUserToSave } from '@poveroh/types'
3835
import { useUser } from '@/hooks/useUser'
39-
40-
const userService = new UserService()
36+
import { toast } from '@poveroh/ui/components/sonner'
4137

4238
export default function ProfileView() {
4339
const t = useTranslations()
44-
const router = useRouter()
4540

46-
const { user, setUser } = useUser()
41+
const { user, saveUser } = useUser()
4742
const [loading, setLoading] = useState(false)
4843

4944
const formSchema = z.object({
@@ -67,22 +62,13 @@ export default function ProfileView() {
6762
form.reset(user)
6863
}, [user, form])
6964

70-
const saveUser = async (userToSave: IUserToSave) => {
65+
const save = async (userToSave: IUserToSave) => {
7166
setLoading(true)
72-
await userService
73-
.save(userToSave)
74-
.then(() => {
75-
toast.success(t('settings.account.personalInfo.form.generalities.messages.success'))
76-
77-
setUser({ ...user, ...userToSave })
7867

79-
if (!isEqual(user.email, userToSave.email)) {
80-
router.push('/logout')
81-
}
82-
})
83-
.catch(error => {
84-
toast.error(error)
85-
})
68+
const res = await saveUser(userToSave)
69+
if (res) {
70+
toast.success(t('form.messages.userSavedSuccess'))
71+
}
8672

8773
setLoading(false)
8874
}
@@ -108,21 +94,26 @@ export default function ProfileView() {
10894
</Breadcrumb>
10995
</div>
11096
<div className='flex flex-col space-y-3'>
111-
<h4>{t('settings.account.personalInfo.form.generalities.title')}</h4>
97+
<h4>{t('settings.account.personalInfo.title')}</h4>
11298
<Box>
11399
<Form {...form}>
114-
<form onSubmit={form.handleSubmit(saveUser)} className='flex flex-col space-y-7 w-full'>
115-
<div className='flex flex-row gap-7 w-full'>
100+
<form onSubmit={form.handleSubmit(save)} className='flex flex-col space-y-7 w-full'>
101+
<FormItem>
102+
<FormLabel>{t('form.id.label')}</FormLabel>
103+
<FormControl>
104+
<CopyableInput value={user.id} />
105+
</FormControl>
106+
<FormMessage />
107+
</FormItem>
108+
<div className='flex flex-row space-x-2 w-full'>
116109
<FormField
117110
control={form.control}
118111
name='name'
119112
render={({ field }) => (
120113
<FormItem>
121-
<FormLabel>
122-
{t('settings.account.personalInfo.form.generalities.name')}
123-
</FormLabel>
114+
<FormLabel mandatory>{t('form.name.label')}</FormLabel>
124115
<FormControl>
125-
<Input {...field} />
116+
<Input {...field} placeholder={t('form.name.placeholder')} />
126117
</FormControl>
127118
<FormMessage />
128119
</FormItem>
@@ -134,30 +125,26 @@ export default function ProfileView() {
134125
name='surname'
135126
render={({ field }) => (
136127
<FormItem>
137-
<FormLabel>
138-
{t('settings.account.personalInfo.form.generalities.surname')}
139-
</FormLabel>
128+
<FormLabel mandatory>{t('form.surname.label')}</FormLabel>
140129
<FormControl>
141-
<Input {...field} />
130+
<Input {...field} placeholder={t('form.surname.placeholder')} />
142131
</FormControl>
143132
<FormMessage />
144133
</FormItem>
145134
)}
146135
/>
147136
</div>
148-
<div className='flex flex-col space-y-3'>
137+
<div className='flex flex-row space-x-2 w-full'>
149138
<FormField
150139
control={form.control}
151140
name='email'
152141
render={({ field }) => (
153142
<FormItem>
154-
<FormLabel>E-mail</FormLabel>
143+
<FormLabel>{t('form.email.label')}</FormLabel>
155144
<FormControl>
156-
<Input placeholder='example@mail.com' {...field} />
145+
<Input placeholder={t('form.email.placeholder')} {...field} />
157146
</FormControl>
158-
<FormDescription>
159-
{t('settings.account.personalInfo.form.generalities.email.subTitle')}
160-
</FormDescription>
147+
<FormDescription>{t('form.email.subTitle')}</FormDescription>
161148
<FormMessage />
162149
</FormItem>
163150
)}

0 commit comments

Comments
 (0)