From 27f81ba11ebee87713e1193b97a2ce64ef64d40c Mon Sep 17 00:00:00 2001 From: Shreyam Kundu <152320199+ShreyamKundu@users.noreply.github.com> Date: Sun, 8 Dec 2024 12:01:04 +0530 Subject: [PATCH 1/4] feat(api): Add logout endpoint to clear token cookie (#581) --- api-collection/Auth Controller/Logout.bru | 18 ++++++++++++++++++ .../api/src/auth/controller/auth.controller.ts | 6 ++++++ apps/api/src/auth/service/auth.service.ts | 12 ++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 api-collection/Auth Controller/Logout.bru diff --git a/api-collection/Auth Controller/Logout.bru b/api-collection/Auth Controller/Logout.bru new file mode 100644 index 00000000..3b1542c4 --- /dev/null +++ b/api-collection/Auth Controller/Logout.bru @@ -0,0 +1,18 @@ +meta { + name: Logout + type: http + seq: 6 +} + +post { + url: {{BASE_URL}}/api/auth/logout + body: none + auth: none +} + + +docs { + ## Description + + This endpoint clears the token cookie, ensuring the user is logged out securely. +} \ No newline at end of file diff --git a/apps/api/src/auth/controller/auth.controller.ts b/apps/api/src/auth/controller/auth.controller.ts index d3411477..12c10a7d 100644 --- a/apps/api/src/auth/controller/auth.controller.ts +++ b/apps/api/src/auth/controller/auth.controller.ts @@ -207,4 +207,10 @@ export class AuthController { ) } } + + @Post('logout') + async logout(@Res() res: Response): Promise { + await this.authService.logout(res) + res.status(HttpStatus.OK).send({ message: 'Logged out successfully' }) + } } diff --git a/apps/api/src/auth/service/auth.service.ts b/apps/api/src/auth/service/auth.service.ts index 2fec728c..702ab4c6 100644 --- a/apps/api/src/auth/service/auth.service.ts +++ b/apps/api/src/auth/service/auth.service.ts @@ -17,6 +17,7 @@ import { CacheService } from '@/cache/cache.service' import { generateOtp } from '@/common/util' import { createUser, getUserByEmailOrId } from '@/common/user' import { UserWithWorkspace } from '@/user/user.types' +import { Response } from 'express' @Injectable() export class AuthService { @@ -219,4 +220,15 @@ export class AuthService { private async generateToken(id: string) { return await this.jwt.signAsync({ id }) } + + /** + * Clears the token cookie on logout + * @param res The response object + */ + async logout(res: Response): Promise { + res.clearCookie('token', { + domain: process.env.DOMAIN ?? 'localhost' + }) + this.logger.log('User logged out and token cookie cleared.') + } } From cb6bbcbac97427286e3e149c211438ca77c7438e Mon Sep 17 00:00:00 2001 From: Allan Joston Fernandes <54631653+Allan2000-Git@users.noreply.github.com> Date: Sun, 8 Dec 2024 18:00:52 +0530 Subject: [PATCH 2/4] feat(api): Add email template for sending OTP to the user (#582) --- .../src/mail/emails/otp-email-template.tsx | 34 ++++++++++++ .../src/mail/emails/styles/common-styles.ts | 12 +++- apps/api/src/mail/services/mail.service.ts | 55 ++++++------------- 3 files changed, 61 insertions(+), 40 deletions(-) create mode 100644 apps/api/src/mail/emails/otp-email-template.tsx diff --git a/apps/api/src/mail/emails/otp-email-template.tsx b/apps/api/src/mail/emails/otp-email-template.tsx new file mode 100644 index 00000000..bcafa17e --- /dev/null +++ b/apps/api/src/mail/emails/otp-email-template.tsx @@ -0,0 +1,34 @@ +import * as React from 'react' +import { Text, Section } from '@react-email/components' +import { otpStyle, text, workspaceDetails } from './styles/common-styles' +import BaseEmailTemplate from './components/base-email-template' + +interface OTPEmailTemplateProps { + otp: string +} + +export const OTPEmailTemplate = ({ otp }: OTPEmailTemplateProps) => { + return ( + + Dear User, + + We’ve sent you this email to verify your Keyshade account. Your One-Time + Password (OTP) is: + +
+ + {otp} + +
+ + This OTP will expire in 5 minutes. Please use it to + complete your action on Keyshade. + +
+ ) +} + +export default OTPEmailTemplate diff --git a/apps/api/src/mail/emails/styles/common-styles.ts b/apps/api/src/mail/emails/styles/common-styles.ts index 027deedb..4602388c 100644 --- a/apps/api/src/mail/emails/styles/common-styles.ts +++ b/apps/api/src/mail/emails/styles/common-styles.ts @@ -35,10 +35,11 @@ export const text: CSSProperties = { export const workspaceDetails: CSSProperties = { width: '100%', - backgroundColor: '#fafafa', borderRadius: '5px', margin: '20px 0px', - padding: '10px 20px' + padding: '10px 20px', + border: '1px solid #eee', + backgroundColor: '#fafafa' } export const workspaceInfo: CSSProperties = { @@ -60,6 +61,13 @@ export const ctaButton: CSSProperties = { borderRadius: '5px' } +export const otpStyle: CSSProperties = { + ...workspaceInfo, + fontSize: '26px', + textAlign: 'center', + letterSpacing: '8px' +} + export const footer: CSSProperties = { borderTop: '1px solid #eaeaea', padding: '20px' diff --git a/apps/api/src/mail/services/mail.service.ts b/apps/api/src/mail/services/mail.service.ts index 3a7dca54..e19a56d4 100644 --- a/apps/api/src/mail/services/mail.service.ts +++ b/apps/api/src/mail/services/mail.service.ts @@ -8,6 +8,7 @@ import { Transporter, createTransport } from 'nodemailer' import RemovedFromWorkspaceEmail from '../emails/workspace-removal' import { render } from '@react-email/render' import WorkspaceInvitationEmail from '../emails/workspace-invitation' +import OTPEmailTemplate from '../emails/otp-email-template' @Injectable() export class MailService implements IMailService { @@ -50,47 +51,25 @@ export class MailService implements IMailService { } async sendOtp(email: string, otp: string): Promise { - const subject = 'Your Login OTP' - const body = ` - - - OTP Verification - - -

Welcome to keyshade!

-

Hello there!

-

We have sent you this email to verify your account.

-

Your One Time Password (OTP) is: ${otp}

-

This OTP will expire in 5 minutes.

-

Please enter this OTP in the application to verify your account.

-

Thank you for choosing us.

-

Best Regards,

-

keyshade Team

- - - ` + const subject = 'Your One Time Password (OTP) for Keyshade' + + const body = await render( + OTPEmailTemplate({ + otp + }) + ) + await this.sendEmail(email, subject, body) } async sendEmailChangedOtp(email: string, otp: string): Promise { - const subject = 'Your OTP for Email Change' - const body = ` - - - OTP Verification - - -

Are you trying to change your email?

-

Hello there!

-

We have sent you this email to verify your new email.

-

Your One Time Password (OTP) is: ${otp}

-

This OTP will expire in 5 minutes.

-

Please enter this OTP in the application to verify your new email.

-

Thank you.

-

Best Regards,

-

keyshade Team

- - - ` + const subject = 'Your Keyshade Email Change One Time Password (OTP)' + + const body = await render( + OTPEmailTemplate({ + otp + }) + ) + await this.sendEmail(email, subject, body) } async accountLoginEmail(email: string): Promise { From 8199de84a1028128cb033287dbc8304c68ac12ad Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Mon, 9 Dec 2024 00:54:37 +0530 Subject: [PATCH 3/4] fix(platform): Type error in navbar --- apps/platform/src/components/shared/navbar/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/platform/src/components/shared/navbar/index.tsx b/apps/platform/src/components/shared/navbar/index.tsx index 82981b63..693aa8bd 100644 --- a/apps/platform/src/components/shared/navbar/index.tsx +++ b/apps/platform/src/components/shared/navbar/index.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from 'react' import { usePathname } from 'next/navigation' import Link from 'next/link' import { DropdownSVG } from '@public/svg/shared' +import type { User } from '@keyshade/schema' import SearchModel from './searchModel' import { DropdownMenu, @@ -16,7 +17,6 @@ import { } from '@/components/ui/dropdown-menu' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import LineTab from '@/components/ui/line-tab' -import { zUser, type User } from '@/types' interface UserNameImage { name: string | null @@ -32,11 +32,7 @@ async function fetchNameImage(): Promise { credentials: 'include' } ) - const userData: User = (await response.json()) as User - const { success, data } = zUser.safeParse(userData) - if (!success) { - throw new Error('Invalid data') - } + const data: User = (await response.json()) as User return { name: data.name?.split(' ')[0] ?? data.email.split('@')[0], image: data.profilePictureUrl From a33f4b546db772362e1979845e3e0f0f3f6c175c Mon Sep 17 00:00:00 2001 From: Zaheer khan <150288870+zaheer-Khan3260@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:55:48 +0530 Subject: [PATCH 4/4] fix(api): Empty name `""` accepted as a valid name while creating environments (#583) --- .../create.environment/create.environment.ts | 4 +++- .../api/src/environment/environment.e2e.spec.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/api/src/environment/dto/create.environment/create.environment.ts b/apps/api/src/environment/dto/create.environment/create.environment.ts index db1adb9a..ffa62e3f 100644 --- a/apps/api/src/environment/dto/create.environment/create.environment.ts +++ b/apps/api/src/environment/dto/create.environment/create.environment.ts @@ -1,7 +1,9 @@ -import { IsOptional, IsString } from 'class-validator' +import { IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator' export class CreateEnvironment { @IsString() + @IsNotEmpty() + @Matches(/^[a-zA-Z0-9-_]{1,64}$/) name: string @IsString() diff --git a/apps/api/src/environment/environment.e2e.spec.ts b/apps/api/src/environment/environment.e2e.spec.ts index be15ee7e..ad949f1c 100644 --- a/apps/api/src/environment/environment.e2e.spec.ts +++ b/apps/api/src/environment/environment.e2e.spec.ts @@ -172,6 +172,23 @@ describe('Environment Controller Tests', () => { expect(environmentFromDb).toBeDefined() }) + it('should not be able to create an environment with an empty name', async () => { + const response = await app.inject({ + method: 'POST', + url: `/environment/${project1.slug}`, + payload: { + name: '', + description: 'Empty name test' + }, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(400) + expect(response.json().message).toContain('name should not be empty') + }) + it('should not be able to create an environment in a project that does not exist', async () => { const response = await app.inject({ method: 'POST',