api/frontend: carry the activation code as a string end to end
Codes starting with a zero were destroyed by the numeric round trip between the app and the API, so those accounts could never be activated. The code is now a validated string from the screen to the lookup, with malformed input reported before any lookup runs
Pedro Lucas Porcellis porcellis@eletrotupi.com · 25 days ago
49e58e036a013f6830a02e9756dd173a285b7e5c Browse filesParents
8f18bacChanged files
6
Additions
+35
Deletions
-12
api/src/controllers/auth.ts
1 1
import { Request, Response, NextFunction } from "express";
2 2
import { AuthenticatedRequest } from "@app/middleware/auth";
3 3
4 4
import {
5 5
login,
150 150
151 151
if (!parsed.success) {
152 152
return res.status(400).json(formatValidationError(parsed.error!));
153 153
}
154 154
155 -
const user = await findUserByActivationCode(String(parsed.data.code));
155 +
const user = await findUserByActivationCode(parsed.data.code);
156 156
157 157
if (!user) {
158 158
return res.status(422).json({
159 159
error: "Código incorreto. Verifique o código e tente novamente",
160 160
});
198 198
} catch (err: any) {
199 199
next(err);
200 200
}
201 201
},
202 202
};
api/src/schemas/auth.schema.ts
1 1
import { z } from 'zod';
2 2
3 3
export const LoginSchema = z.object({
4 4
email: z.string().email(),
5 5
password: z.string().min(1)
13 13
token: z.string(),
14 14
newPassword: z.string().min(6)
15 15
});
16 16
17 17
export const ActivateUserSchema = z.object({
18 -
code: z.number()
18 +
code: z.string().regex(/^\d{6}$/)
19 19
});
20 20
21 21
export type LoginInput = z.infer<typeof LoginSchema>;
22 22
export type PasswordResetRequestInput = z.infer<typeof PasswordResetRequestSchema>;
23 23
export type PasswordResetInput = z.infer<typeof PasswordResetSchema>;
24 24
export type ActivateUserInput = z.infer<typeof ActivateUserSchema>;
api/tests/auth/activate.test.ts
1 1
import { describe, it, expect, afterAll, beforeEach } from 'vitest'
2 2
import request from 'supertest'
3 3
import { createApp } from '@app/createApp'
4 4
import {
5 5
buildPrisma,
33 33
const user = await createTestUser(db, { email: 'activate@example.com' })
34 34
await seedActivationCode(user.id)
35 35
36 36
const res = await request(app)
37 37
.post('/auth/activate')
38 -
.send({ code: 123456 })
38 +
.send({ code: '123456' })
39 39
40 40
expect(res.status).toBe(200)
41 41
expect(res.body.user.active).toBe(true)
42 42
expect(res.body.user.activationCode).toBeNull()
43 43
44 44
const dbUser = await db.user.findUnique({ where: { id: user.id } })
45 45
expect(dbUser!.active).toBe(true)
46 46
expect(dbUser!.activationCode).toBeNull()
47 47
})
48 48
49 -
it('returns 422 for an unknown code', async () => {
49 +
it('activates an account whose code begins with zero', async () => {
50 +
const user = await createTestUser(db, { email: 'leading-zero@example.com' })
51 +
await seedActivationCode(user.id, '012345')
52 +
53 +
const res = await request(app)
54 +
.post('/auth/activate')
55 +
.send({ code: '012345' })
56 +
57 +
expect(res.status).toBe(200)
58 +
expect(res.body.user.active).toBe(true)
59 +
60 +
const dbUser = await db.user.findUnique({ where: { id: user.id } })
61 +
expect(dbUser!.active).toBe(true)
62 +
expect(dbUser!.activationCode).toBeNull()
63 +
})
64 +
65 +
it('returns 422 for a well-formed but unknown code', async () => {
50 66
await createTestUser(db, { email: 'activate@example.com' })
51 67
52 68
const res = await request(app)
53 69
.post('/auth/activate')
54 -
.send({ code: 999999 })
70 +
.send({ code: '999999' })
55 71
56 72
expect(res.status).toBe(422)
57 73
expect(res.body.error).toBeTruthy()
58 74
})
59 75
60 -
it('returns 400 when the code is not a number', async () => {
76 +
it.each([
77 +
['too short', '12345'],
78 +
['too long', '1234567'],
79 +
['non-digit', '12a456'],
80 +
['a number', 123456],
81 +
])('rejects a code that is %s as malformed', async (_shape, code) => {
82 +
await createTestUser(db, { email: 'activate@example.com' })
83 +
61 84
const res = await request(app)
62 85
.post('/auth/activate')
63 -
.send({ code: '123456' })
86 +
.send({ code })
64 87
65 88
expect(res.status).toBe(400)
66 -
expect(res.body.fields).toBeTruthy()
89 +
expect(res.body.fields.code).toBeTruthy()
67 90
})
68 91
})
69 92
70 93
describe('POST /auth/resend_code', () => {
71 94
it('issues a fresh activation code for the authenticated user', async () => {
86 109
it('returns 401 without a token', async () => {
87 110
const res = await request(app).post('/auth/resend_code')
88 111
expect(res.status).toBe(401)
89 112
})
90 113
})
frontend/app/auth/activate.tsx
1 1
import React, { useState } from 'react';
2 2
import {
3 3
View,
4 4
StyleSheet,
5 5
ScrollView,
46 46
const handleActivate = async () => {
47 47
if (!validateForm()) return;
48 48
49 49
setLoading(true);
50 50
try {
51 -
await activate(Number(code.replaceAll(' ', '')));
51 +
await activate(code);
52 52
showToast('Conta ativada com sucesso!', 'success');
53 53
router.replace('/');
54 54
} catch (error: any) {
55 55
showToast(translateError(error.message) || 'Falha em ativar a conta', 'error');
56 56
} finally {
157 157
footerText: {
158 158
textAlign: 'center',
159 159
marginBottom: 10
160 160
}
161 161
});
frontend/context/AuthContext.tsx
1 1
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
2 2
import { apiClient, User } from "@/lib/api";
3 3
import { queryClient, storage } from "@/lib/queryClient";
4 4
5 5
interface AuthProviderProps {
21 21
logout: () => Promise<void>;
22 22
forgotPassword: (
23 23
email: string,
24 24
) => Promise<{ message: string; token?: string }>;
25 25
resetPassword: (token: string, password: string) => Promise<void>;
26 -
activate: (code: number) => Promise<void>;
26 +
activate: (code: string) => Promise<void>;
27 27
requestActivateCode: () => Promise<void>;
28 28
}
29 29
30 30
export function sanitizeUser(data: any): User {
31 31
return {
135 135
136 136
const resetPassword = async (token: string, password: string) => {
137 137
await apiClient.resetPassword(token, password);
138 138
};
139 139
140 -
const activate = async (code: number) => {
140 +
const activate = async (code: string) => {
141 141
const response = await apiClient.activate(code);
142 142
setUser(response.user);
143 143
};
144 144
145 145
const requestActivateCode = async () => {
168 168
[user, isLoading, isAuthenticated],
169 169
);
170 170
171 171
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
172 172
};
frontend/lib/api/client.ts
1 1
import * as SecureStore from "expo-secure-store";
2 2
3 3
export class ApiError extends Error {
4 4
status: number;
5 5
fields?: Record<string, string>;
154 154
}
155 155
156 156
return response;
157 157
}
158 158
159 -
async activate(code: number): Promise<ActivateResponse> {
159 +
async activate(code: string): Promise<ActivateResponse> {
160 160
return this.request("/auth/activate", {
161 161
method: "POST",
162 162
body: JSON.stringify({ code }),
163 163
});
164 164
}
513 513
return response.arrayBuffer()
514 514
}
515 515
}
516 516
517 517
export const apiClient = new ApiClient();