From 1135d859a9e45e8cd96a91f70f9a65ec43badcd1 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 13 Nov 2023 22:46:21 +0100 Subject: [PATCH] feat: emails can be sent with different locales (#22) --- src/account/account.controller.spec.ts | 4 +++ src/account/account.controller.ts | 38 +++++++++++++++++++++----- src/account/keycloak.service.ts | 10 ++++++- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/account/account.controller.spec.ts b/src/account/account.controller.spec.ts index 0a237cb..6779182 100644 --- a/src/account/account.controller.spec.ts +++ b/src/account/account.controller.spec.ts @@ -59,6 +59,7 @@ describe('AccountController', () => { expect(mockHttp.put).toHaveBeenCalledWith( expect.stringMatching(/\/user-id\/execute-actions-email/), ['VERIFY_EMAIL'], + expect.anything(), ); // set roles expect(mockHttp.post).toHaveBeenCalledWith( @@ -107,6 +108,7 @@ describe('AccountController', () => { `execute-actions-email?client_id=${user.client}`, ), ['VERIFY_EMAIL'], + expect.anything(), ); // old roles are deleted expect(mockHttp.delete).toHaveBeenCalledWith( @@ -137,6 +139,7 @@ describe('AccountController', () => { `execute-actions-email?client_id=${user.client}`, ), ['VERIFY_EMAIL'], + expect.anything(), ], ]); done(); @@ -161,6 +164,7 @@ describe('AccountController', () => { `${user.realm}/users/${user.sub}/execute-actions-email?client_id=${user.client}&redirect_uri=`, ), ['UPDATE_PASSWORD'], + expect.anything(), ); done(); }); diff --git a/src/account/account.controller.ts b/src/account/account.controller.ts index 2a7be8e..740da5a 100644 --- a/src/account/account.controller.ts +++ b/src/account/account.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Get, + Headers, Param, Post, Put, @@ -9,7 +10,7 @@ import { UseGuards, } from '@nestjs/common'; import { BearerGuard } from '../auth/bearer/bearer.guard'; -import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiHeader, ApiOperation } from '@nestjs/swagger'; import { concatMap, concatWith, @@ -51,8 +52,13 @@ export class AccountController { }) @UseGuards(BearerGuard) @ApiBearerAuth() + @ApiHeader({ name: 'Accept-Language', required: false }) @Put('set-email') - setEmail(@Req() req, @Body() { email }: SetEmailReq) { + setEmail( + @Req() req, + @Body() { email }: SetEmailReq, + @Headers('Accept-Language') lang?: string, + ) { const user = req.user as User; // TODO email is directly marked as verified return this.keycloak @@ -68,6 +74,7 @@ export class AccountController { user.client, user.sub, 'VERIFY_EMAIL', + lang, ), ), prepareResult(), @@ -79,12 +86,22 @@ export class AccountController { description: 'Looks for the user with the given email and sends a reset password email', }) + @ApiHeader({ name: 'Accept-Language', required: false }) @Post('forgot-password') - forgotPassword(@Body() { email, realm, client }: ForgotEmailReq) { + forgotPassword( + @Body() { email, realm, client }: ForgotEmailReq, + @Headers('Accept-Language') lang?: string, + ) { return this.keycloak.findUserBy(realm, { email }).pipe( // TODO only verified/valid accounts should allow a password reset? concatMap((user) => - this.keycloak.sendEmail(realm, client, user.id, 'UPDATE_PASSWORD'), + this.keycloak.sendEmail( + realm, + client, + user.id, + 'UPDATE_PASSWORD', + lang, + ), ), prepareResult(), ); @@ -110,10 +127,15 @@ export class AccountController { `, }) @ApiBearerAuth() + @ApiHeader({ name: 'Accept-Language', required: false }) @UseGuards(BearerGuard, RolesGuard) @Roles(AccountController.ACCOUNT_MANAGEMENT_ROLE) @Post() - createAccount(@Req() req, @Body() { username, email, roles }: NewAccount) { + createAccount( + @Req() req, + @Body() { username, email, roles }: NewAccount, + @Headers('Accept-Language') lang?: string, + ) { const user = req.user as User; const { realm, client } = user; let userId: string; @@ -121,7 +143,7 @@ export class AccountController { concatMap(() => this.keycloak.findUserBy(realm, { username })), tap((res) => (userId = res.id)), concatMap(() => - this.keycloak.sendEmail(realm, client, userId, 'VERIFY_EMAIL'), + this.keycloak.sendEmail(realm, client, userId, 'VERIFY_EMAIL', lang), ), concatMap(() => this.keycloak.assignRoles(user.realm, userId, roles)), prepareResult(), @@ -156,6 +178,7 @@ export class AccountController { description: 'Partially update properties of a user.', }) @ApiBearerAuth() + @ApiHeader({ name: 'Accept-Language', required: false }) @UseGuards(BearerGuard, RolesGuard) @Roles(AccountController.ACCOUNT_MANAGEMENT_ROLE) @Put('/:userId') @@ -163,6 +186,7 @@ export class AccountController { @Req() req, @Param('userId') userId: string, @Body() updatedUser: KeycloakUser, + @Headers('Accept-Language') lang?: string, ) { const { realm, client } = req.user as User; const observables: Observable[] = []; @@ -181,7 +205,7 @@ export class AccountController { // send verification email if email changed updatedUser.requiredActions = ['VERIFY_EMAIL']; observables.push( - this.keycloak.sendEmail(realm, client, userId, 'VERIFY_EMAIL'), + this.keycloak.sendEmail(realm, client, userId, 'VERIFY_EMAIL', lang), ); } // first update the user object, then run other observables diff --git a/src/account/keycloak.service.ts b/src/account/keycloak.service.ts index c98188d..a9c876f 100644 --- a/src/account/keycloak.service.ts +++ b/src/account/keycloak.service.ts @@ -84,12 +84,20 @@ export class KeycloakService { * @param client * @param userId * @param action e.g. "UPDATE_PASSWORD", "VERIFY_EMAIL" + * @param lang */ - sendEmail(realm: string, client: string, userId: string, action: string) { + sendEmail( + realm: string, + client: string, + userId: string, + action: string, + lang?: string, + ) { return this.perform( this.http.put, `${realm}/users/${userId}/execute-actions-email?client_id=${client}&redirect_uri=`, [action], + { headers: { 'Accept-Language': lang } }, ); }