source: app/(app)/profile/actions.ts

nextjs
Last change on this file was b04ba1e, checked in by Vasilaki Totsili <vasilaki@…>, 2 days ago

refactor: implement transaction management for db operations

Replaced single transaction operations with transaction blocks for
critical database mutations to ensure atomicity and consistency,
preventing race conditions and ensuring that operations like checking
for existing records and subsequent inserts or updates are atomic:

  • Implement transactional blocks for addTag, updateProfile and register functions to handle duplicate checks and data insertion/updating in a single transaction scope
  • Handle specific error messages in a more controlled manner by checking exception types and messages
  • Property mode set to 100644
File size: 2.8 KB
Line 
1'use server';
2
3import { auth } from '@/auth';
4import { redirect } from 'next/navigation';
5import { sql } from '@/app/lib/db';
6import bcrypt from 'bcrypt';
7
8type ActionResult = string | undefined; // string = error message, undefined = success
9
10export async function updateProfile(
11 _prevState: ActionResult,
12 formData: FormData
13): Promise<ActionResult> {
14 const session = await auth();
15 if (!session?.user?.id) {
16 redirect('/login');
17 }
18
19 const userId = Number(session.user.id);
20 if (!Number.isInteger(userId)) {
21 return 'Invalid session. Please log in again.';
22 }
23 const name = String(formData.get('name') ?? '').trim();
24 const email = String(formData.get('email') ?? '').trim().toLowerCase();
25
26 if (!name) {
27 return 'Name is required.';
28 }
29 if (!email || !email.includes('@')) {
30 return 'Please enter a valid email.';
31 }
32
33 try {
34 // eslint-disable-next-line @typescript-eslint/no-explicit-any
35 await sql.begin(async (tx: any) => {
36 // Email already exists check
37 const existing = await tx`
38 SELECT user_id FROM "user"
39 WHERE email = ${email} AND user_id != ${userId}
40 `;
41 if (existing.length > 0) {
42 throw new Error('Email already exists.');
43 }
44
45 await tx`
46 UPDATE "user"
47 SET user_name = ${name},
48 email = ${email}
49 WHERE user_id = ${userId}
50 `;
51 });
52 } catch (e: any) {
53 if (e instanceof Error && e.message === 'Email already exists.') {
54 return e.message;
55 }
56 throw e;
57 }
58
59 redirect('/profile');
60}
61
62export async function updatePassword(
63 _prevState: ActionResult,
64 formData: FormData
65): Promise<ActionResult> {
66 const session = await auth();
67 if (!session?.user?.id) {
68 redirect('/login');
69 }
70
71 const userId = Number(session.user.id);
72 if (!Number.isInteger(userId)) {
73 return 'Invalid session. Please log in again.';
74 }
75 const currentPassword = String(formData.get('currentPassword') ?? '');
76 const newPassword = String(formData.get('newPassword') ?? '');
77
78 if (newPassword.length < 6) {
79 return 'New password must be at least 6 characters.';
80 }
81
82 const users = await sql`
83 SELECT password
84 FROM "user"
85 WHERE user_id = ${userId}
86 `;
87 const user = users[0];
88 if (!user) {
89 return 'User not found. Please log in again.';
90 }
91
92 const match = await bcrypt.compare(currentPassword, user.password);
93 if (!match) {
94 return 'Current password is incorrect.';
95 }
96
97 const hashed = await bcrypt.hash(newPassword, 10);
98
99 await sql`
100 UPDATE "user"
101 SET password = ${hashed}
102 WHERE user_id = ${userId}
103 `;
104
105 redirect('/profile');
106}
Note: See TracBrowser for help on using the repository browser.