Skip to content

Commit 3460e0e

Browse files
authored
Merge pull request #138 from kitcc-org/136-vld-pwd-upd-user
ユーザ更新時にパスワードのバリデーションをかける
2 parents 49a5744 + 6441799 commit 3460e0e

File tree

9 files changed

+142
-59
lines changed

9 files changed

+142
-59
lines changed

api/bundle.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -673,15 +673,21 @@ paths:
673673
email:
674674
type: string
675675
format: email
676-
password:
676+
currentPassword:
677+
type: string
678+
format: password
679+
pattern: ^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$
680+
minLength: 8
681+
newPassword:
677682
type: string
678683
format: password
679684
pattern: ^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$
680685
minLength: 8
681686
example:
682687
name: 比企谷八幡
683688
email: hikigaya@oregairu.com
684-
password: passw0rd
689+
currentPassword: passw0rd
690+
newPassword: pa55word
685691
responses:
686692
'200':
687693
description: 情報の更新に成功した
@@ -1108,3 +1114,4 @@ components:
11081114
id: 1
11091115
name: 比企谷八幡
11101116
email: hikigaya@oregairu.com
1117+
sessionToken: abcde12345

api/components/examples/user.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
value:
22
id: 1
3-
name: "比企谷八幡"
4-
email: "hikigaya@oregairu.com"
3+
name: '比企谷八幡'
4+
email: 'hikigaya@oregairu.com'
5+
sessionToken: 'abcde12345'

api/paths/user.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,21 @@ user:
221221
email:
222222
type: string
223223
format: email
224-
password:
224+
currentPassword:
225+
type: string
226+
format: password
227+
pattern: ^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$
228+
minLength: 8
229+
newPassword:
225230
type: string
226231
format: password
227232
pattern: ^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$
228233
minLength: 8
229234
example:
230235
name: '比企谷八幡'
231236
email: 'hikigaya@oregairu.com'
232-
password: 'passw0rd'
237+
currentPassword: 'passw0rd'
238+
newPassword: 'pa55word'
233239
responses:
234240
'200':
235241
description: 情報の更新に成功した

backend/src/api/user.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { zValidator } from '@hono/zod-validator';
33
import { and, asc, eq, inArray, like } from 'drizzle-orm';
44
import { drizzle } from 'drizzle-orm/d1';
55
import { Hono } from 'hono';
6+
import { getCookie } from 'hono/cookie';
67
import {
78
createUserBody,
89
deleteUserParams,
@@ -274,14 +275,66 @@ app.patch(
274275
const param = ctx.req.valid('param');
275276
const id = parseInt(param['userId']);
276277

277-
const user = ctx.req.valid('json');
278+
const userIdCookie = getCookie(ctx, 'user_id', 'secure');
279+
const userId = Number(userIdCookie);
280+
281+
if (id !== userId) {
282+
// ログインユーザと更新対象のユーザが異なる
283+
return ctx.json(
284+
{
285+
message: 'Unauthorized',
286+
},
287+
401,
288+
);
289+
}
290+
291+
const newUser = ctx.req.valid('json');
278292

279293
const db = drizzle(ctx.env.DB);
280-
let updatedBook: SelectUser[] = [];
294+
295+
if (newUser.newPassword) {
296+
// 新しいパスワードが指定されている
297+
if (!newUser.currentPassword) {
298+
// 現在のパスワードが指定されていない
299+
return ctx.json(
300+
{
301+
message: 'Current password is required to update password',
302+
},
303+
400,
304+
);
305+
}
306+
307+
const user = await db
308+
.select()
309+
.from(userTable)
310+
.where(eq(userTable.id, id));
311+
312+
if (user.length === 0) {
313+
return ctx.notFound();
314+
}
315+
316+
const digest = await generateHash(newUser.currentPassword);
317+
if (digest != user[0].passwordDigest) {
318+
return ctx.json(
319+
{
320+
message: 'Current password is incorrect',
321+
},
322+
400,
323+
);
324+
}
325+
}
326+
327+
let updatedUser: SelectUser[] = [];
281328
try {
282-
updatedBook = await db
329+
updatedUser = await db
283330
.update(userTable)
284-
.set(user)
331+
.set({
332+
name: newUser.name,
333+
email: newUser.email,
334+
passwordDigest: newUser.newPassword
335+
? await generateHash(newUser.newPassword)
336+
: undefined,
337+
})
285338
.where(eq(userTable.id, id))
286339
.returning();
287340
} catch (err) {
@@ -295,11 +348,11 @@ app.patch(
295348
}
296349
}
297350

298-
if (updatedBook.length === 0) {
351+
if (updatedUser.length === 0) {
299352
return ctx.notFound();
300353
}
301354

302-
const result = updateUserResponse.safeParse(updatedBook[0]);
355+
const result = updateUserResponse.safeParse(updatedUser[0]);
303356
if (!result.success) {
304357
console.error(result.error);
305358
return ctx.json(
@@ -342,12 +395,12 @@ app.delete(
342395
const id = parseInt(param['userId']);
343396

344397
const db = drizzle(ctx.env.DB);
345-
const deletedBook = await db
398+
const deletedUser = await db
346399
.delete(userTable)
347400
.where(eq(userTable.id, id))
348401
.returning();
349402

350-
if (deletedBook.length === 0) {
403+
if (deletedUser.length === 0) {
351404
return ctx.notFound();
352405
}
353406

backend/src/schema.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,15 +261,19 @@ export const updateUserParams = zod.object({
261261
"userId": zod.string().regex(updateUserPathUserIdRegExp)
262262
})
263263

264-
export const updateUserBodyPasswordMin = 8;
264+
export const updateUserBodyCurrentPasswordMin = 8;
265265

266-
export const updateUserBodyPasswordRegExp = new RegExp('^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]+$');
266+
export const updateUserBodyCurrentPasswordRegExp = new RegExp('^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]+$');
267+
export const updateUserBodyNewPasswordMin = 8;
268+
269+
export const updateUserBodyNewPasswordRegExp = new RegExp('^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]+$');
267270

268271

269272
export const updateUserBody = zod.object({
270273
"name": zod.string().optional(),
271274
"email": zod.string().email().optional(),
272-
"password": zod.string().min(updateUserBodyPasswordMin).regex(updateUserBodyPasswordRegExp).optional()
275+
"currentPassword": zod.string().min(updateUserBodyCurrentPasswordMin).regex(updateUserBodyCurrentPasswordRegExp).optional(),
276+
"newPassword": zod.string().min(updateUserBodyNewPasswordMin).regex(updateUserBodyNewPasswordRegExp).optional()
273277
})
274278

275279
export const updateUserResponse = zod.object({

backend/test/api/user.test.ts

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,12 @@ describe('PATCH /users/:userId', () => {
113113
const credentials = {
114114
name: '比企谷八幡',
115115
email: 'hikigaya@oregairu.com',
116-
password: 'passw0rd',
116+
currentPassword: 'passw0rd',
117+
newPassword: 'pa55word',
117118
};
118119

119-
const users = await db.select().from(userTable);
120120
const response = await app.request(
121-
`/users/${users[0].id}`,
121+
`/users/${currentUser.id}`,
122122
{
123123
method: 'PATCH',
124124
headers: {
@@ -139,9 +139,9 @@ describe('PATCH /users/:userId', () => {
139139
const updatedUser = await db
140140
.select()
141141
.from(userTable)
142-
.where(eq(userTable.id, users[0].id));
143-
const { password, ...rest } = credentials;
144-
expect(updatedUser[0]).toMatchObject(rest);
142+
.where(eq(userTable.id, currentUser.id!));
143+
expect(updatedUser[0].name).toBe(credentials.name);
144+
expect(updatedUser[0].email).toBe(credentials.email);
145145
});
146146

147147
loggedInTest(
@@ -172,7 +172,7 @@ describe('PATCH /users/:userId', () => {
172172
'should return 400 when name is not a string',
173173
async ({ currentUser, sessionToken }) => {
174174
const response = await app.request(
175-
`/users/1`,
175+
`/users/${currentUser.id}`,
176176
{
177177
method: 'PATCH',
178178
headers: {
@@ -196,7 +196,7 @@ describe('PATCH /users/:userId', () => {
196196
'should return 400 when email is invalid',
197197
async ({ currentUser, sessionToken }) => {
198198
const response = await app.request(
199-
`/users/1`,
199+
`/users/${currentUser.id}`,
200200
{
201201
method: 'PATCH',
202202
headers: {
@@ -206,7 +206,7 @@ describe('PATCH /users/:userId', () => {
206206
`__Secure-session_token=${sessionToken}`,
207207
].join('; '),
208208
},
209-
// メールアドレスに文字列以外を指定する
209+
// メールアドレスが形式に合っていない
210210
body: JSON.stringify({ email: 'user@invalid' }),
211211
},
212212
env,
@@ -220,14 +220,14 @@ describe('PATCH /users/:userId', () => {
220220
'should return 400 when password is invalid',
221221
async ({ currentUser, sessionToken }) => {
222222
const invalidPassword = [
223-
'abc123', // 文字数が8未満
223+
'abc123', // 文字数が8文字未満
224224
'12345678', // 英字が含まれていない
225225
'password', // 数字が含まれていない
226226
];
227227

228228
for (const password of invalidPassword) {
229229
const response = await app.request(
230-
`/users/1`,
230+
`/users/${currentUser.id}`,
231231
{
232232
method: 'PATCH',
233233
headers: {
@@ -238,7 +238,7 @@ describe('PATCH /users/:userId', () => {
238238
].join('; '),
239239
},
240240
// パスワードに文字列以外を指定する
241-
body: JSON.stringify({ password: 1 }),
241+
body: JSON.stringify({ password: password }),
242242
},
243243
env,
244244
);
@@ -249,12 +249,10 @@ describe('PATCH /users/:userId', () => {
249249
);
250250

251251
loggedInTest(
252-
'should return 400 when violate email unique constraint',
252+
'should return 400 when current password is incorrect',
253253
async ({ currentUser, sessionToken }) => {
254-
const users = await db.select().from(userTable);
255-
256254
const response = await app.request(
257-
`/users/${users[1].id}`,
255+
`/users/${currentUser.id}`,
258256
{
259257
method: 'PATCH',
260258
headers: {
@@ -264,8 +262,10 @@ describe('PATCH /users/:userId', () => {
264262
`__Secure-session_token=${sessionToken}`,
265263
].join('; '),
266264
},
267-
// 既に存在するメールアドレスを指定する
268-
body: JSON.stringify({ email: users[0].email }),
265+
body: JSON.stringify({
266+
currentPassword: 'abcd1234',
267+
newPassword: 'pa55word',
268+
}),
269269
},
270270
env,
271271
);
@@ -274,25 +274,13 @@ describe('PATCH /users/:userId', () => {
274274
},
275275
);
276276

277-
it('should return 401 when not logged in', async () => {
278-
// prettier-ignore
279-
const response = await app.request(
280-
`/users/1`,
281-
{
282-
method: 'PATCH',
283-
body: JSON.stringify({ name: '比企谷八幡' }),
284-
}
285-
);
286-
287-
expect(response.status).toBe(401);
288-
});
289-
290277
loggedInTest(
291-
'should return 404 when user is not found',
278+
'should return 400 when violate email unique constraint',
292279
async ({ currentUser, sessionToken }) => {
280+
const users = await db.select().from(userTable);
281+
293282
const response = await app.request(
294-
// 存在しないuserIdを指定する
295-
`/users/100`,
283+
`/users/${currentUser.id}`,
296284
{
297285
method: 'PATCH',
298286
headers: {
@@ -302,14 +290,28 @@ describe('PATCH /users/:userId', () => {
302290
`__Secure-session_token=${sessionToken}`,
303291
].join('; '),
304292
},
305-
body: JSON.stringify({ name: 'username' }),
293+
// 既に存在するメールアドレスを指定する
294+
body: JSON.stringify({ email: users[0].email }),
306295
},
307296
env,
308297
);
309298

310-
expect(response.status).toBe(404);
299+
expect(response.status).toBe(400);
311300
},
312301
);
302+
303+
it('should return 401 when not logged in', async () => {
304+
// prettier-ignore
305+
const response = await app.request(
306+
`/users/1`,
307+
{
308+
method: 'PATCH',
309+
body: JSON.stringify({ name: '比企谷八幡' }),
310+
}
311+
);
312+
313+
expect(response.status).toBe(401);
314+
});
313315
});
314316

315317
describe('DELETE /users/:userId', () => {

frontend/client/client.schemas.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,18 @@ export type DeleteUser204 = {
8686
};
8787

8888
export type UpdateUserBody = {
89+
/**
90+
* @minLength 8
91+
* @pattern ^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$
92+
*/
93+
currentPassword?: string;
8994
email?: string;
9095
name?: string;
9196
/**
9297
* @minLength 8
9398
* @pattern ^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]+$
9499
*/
95-
password?: string;
100+
newPassword?: string;
96101
};
97102

98103
export type DeleteUsersBody = {

0 commit comments

Comments
 (0)