diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index f36ea646..3bbabf7d 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -30,6 +30,12 @@ export class AuthController { getOrgFromCookie ); + if (body.provider === 'LOCAL') { + response.header('activate', 'true'); + response.status(200).json({ activate: true }); + return; + } + response.cookie('auth', jwt, { domain: '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname, @@ -132,6 +138,29 @@ export class AuthController { return this._authService.oauthLink(provider); } + @Post('/activate') + async activate( + @Body('code') code: string, + @Res({ passthrough: true }) response: Response + ) { + const activate = await this._authService.activate(code); + if (!activate) { + return response.status(200).send({ can: false }); + } + + response.cookie('auth', activate, { + domain: + '.' + new URL(removeSubdomain(process.env.FRONTEND_URL!)).hostname, + secure: true, + httpOnly: true, + sameSite: 'none', + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), + }); + + response.header('onboarding', 'true'); + return response.status(200).send({ can: true }); + } + @Post('/oauth/:provider/exists') async oauthExists( @Body('code') code: string, diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index cbad49fd..5cc8f4c0 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -14,7 +14,7 @@ async function bootstrap() { rawBody: true, cors: { credentials: true, - exposedHeaders: ['reload', 'onboarding'], + exposedHeaders: ['reload', 'onboarding', 'activate'], origin: [process.env.FRONTEND_URL], } }); diff --git a/apps/backend/src/services/auth/auth.middleware.ts b/apps/backend/src/services/auth/auth.middleware.ts index 372b40c1..a54dfb4a 100644 --- a/apps/backend/src/services/auth/auth.middleware.ts +++ b/apps/backend/src/services/auth/auth.middleware.ts @@ -36,8 +36,11 @@ export class AuthMiddleware implements NestMiddleware { const orgHeader = req.cookies.showorg || req.headers.showorg; if (!user) { - removeAuth(res); - res.status(401).send('Unauthorized'); + throw new HttpForbiddenException(); + } + + if (!user.activated) { + throw new HttpForbiddenException(); } if (user?.isSuperAdmin && req.cookies.impersonate) { diff --git a/apps/backend/src/services/auth/auth.service.ts b/apps/backend/src/services/auth/auth.service.ts index 007fc746..160a7f30 100644 --- a/apps/backend/src/services/auth/auth.service.ts +++ b/apps/backend/src/services/auth/auth.service.ts @@ -10,13 +10,15 @@ import dayjs from 'dayjs'; import { NewsletterService } from '@gitroom/nestjs-libraries/services/newsletter.service'; import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service'; import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto'; +import { EmailService } from '@gitroom/nestjs-libraries/services/email.service'; @Injectable() export class AuthService { constructor( private _userService: UsersService, private _organizationService: OrganizationService, - private _notificationService: NotificationService + private _notificationService: NotificationService, + private _emailService: EmailService, ) {} async routeAuth( provider: Provider, @@ -31,7 +33,7 @@ export class AuthService { } const create = await this._organizationService.createOrgAndUser(body); - NewsletterService.register(body.email); + const addedOrg = addToOrg && typeof addToOrg !== 'boolean' ? await this._organizationService.addUserToOrg( @@ -41,14 +43,21 @@ export class AuthService { addToOrg.role ) : false; - return { addedOrg, jwt: await this.jwt(create.users[0].user) }; + + const obj = { addedOrg, jwt: await this.jwt(create.users[0].user) }; + await this._emailService.sendEmail(body.email, 'Activate your account', `Click here to activate your account`); + return obj; } if (!user || !AuthChecker.comparePassword(body.password, user.password)) { - throw new Error('Invalid user'); + throw new Error('Invalid user name or password'); } - return { jwt: await this.jwt(user) }; + if (!user.activated) { + throw new Error('User is not activated'); + } + + return { addedOrg: false, jwt: await this.jwt(user) }; } const user = await this.loginOrRegisterProvider( @@ -152,6 +161,22 @@ export class AuthService { return this._userService.updatePassword(user.id, body.password); } + async activate(code: string) { + const user = AuthChecker.verifyJWT(code) as { id: string, activated: boolean, email: string }; + if (user.id && !user.activated) { + const getUserAgain = await this._userService.getUserByEmail(user.email); + if (getUserAgain.activated) { + return false; + } + await this._userService.activateUser(user.id); + user.activated = true; + await NewsletterService.register(user.email); + return this.jwt(user as any); + } + + return false; + } + oauthLink(provider: string) { const providerInstance = ProvidersFactory.loadProvider( provider as Provider diff --git a/apps/frontend/src/app/auth/activate/[code]/page.tsx b/apps/frontend/src/app/auth/activate/[code]/page.tsx new file mode 100644 index 00000000..fb02d53a --- /dev/null +++ b/apps/frontend/src/app/auth/activate/[code]/page.tsx @@ -0,0 +1,15 @@ +import { isGeneral } from '@gitroom/react/helpers/is.general'; + +export const dynamic = 'force-dynamic'; + +import { Metadata } from 'next'; +import { AfterActivate } from '@gitroom/frontend/components/auth/after.activate'; + +export const metadata: Metadata = { + title: `${isGeneral() ? 'Postiz' : 'Gitroom'} - Activate your account`, + description: '', +}; + +export default async function Auth() { + return ; +} diff --git a/apps/frontend/src/app/auth/activate/page.tsx b/apps/frontend/src/app/auth/activate/page.tsx new file mode 100644 index 00000000..607b709d --- /dev/null +++ b/apps/frontend/src/app/auth/activate/page.tsx @@ -0,0 +1,17 @@ +import { isGeneral } from '@gitroom/react/helpers/is.general'; + +export const dynamic = 'force-dynamic'; + +import {Metadata} from "next"; +import { Activate } from '@gitroom/frontend/components/auth/activate'; + +export const metadata: Metadata = { + title: `${isGeneral() ? 'Postiz' : 'Gitroom'} - Activate your account`, + description: '', +}; + +export default async function Auth() { + return ( + + ); +} diff --git a/apps/frontend/src/components/auth/activate.tsx b/apps/frontend/src/components/auth/activate.tsx new file mode 100644 index 00000000..cdf18d1e --- /dev/null +++ b/apps/frontend/src/components/auth/activate.tsx @@ -0,0 +1,16 @@ +'use client'; + +export function Activate() { + return ( + <> +
+

+ Activate your account +

+
+
+ Thank you for registering!
Please check your email to activate your account. +
+ + ); +} diff --git a/apps/frontend/src/components/auth/after.activate.tsx b/apps/frontend/src/components/auth/after.activate.tsx new file mode 100644 index 00000000..f19a8784 --- /dev/null +++ b/apps/frontend/src/components/auth/after.activate.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useParams } from 'next/navigation'; +import Link from 'next/link'; + +export const AfterActivate = () => { + const fetch = useFetch(); + const params = useParams(); + const [showLoader, setShowLoader] = useState(true); + const run = useRef(false); + + useEffect(() => { + if (!run.current) { + run.current = true; + loadCode(); + } + }, []); + + const loadCode = useCallback(async () => { + if (params.code) { + const { can } = await ( + await fetch(`/auth/activate`, { + method: 'POST', + body: JSON.stringify({ code: params.code }), + headers: { + 'Content-Type': 'application/json', + }, + }) + ).json(); + + if (!can) { + setShowLoader(false); + } + } + }, []); + + return ( + <>{showLoader ? : (<>This user is already activated,
Click here to go back to login)} + ); +}; diff --git a/apps/frontend/src/components/auth/login.tsx b/apps/frontend/src/components/auth/login.tsx index 5f101a37..594659cd 100644 --- a/apps/frontend/src/components/auth/login.tsx +++ b/apps/frontend/src/components/auth/login.tsx @@ -44,7 +44,7 @@ export function Login() { if (login.status === 400) { form.setError('email', { - message: 'Invalid email or password', + message: await login.text(), }); setLoading(false); diff --git a/apps/frontend/src/components/auth/register.tsx b/apps/frontend/src/components/auth/register.tsx index bf8242a2..3d160ad2 100644 --- a/apps/frontend/src/components/auth/register.tsx +++ b/apps/frontend/src/components/auth/register.tsx @@ -9,7 +9,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { classValidatorResolver } from '@hookform/resolvers/class-validator'; import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto'; import { GithubProvider } from '@gitroom/frontend/components/auth/providers/github.provider'; -import { useSearchParams } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; import interClass from '@gitroom/react/helpers/inter.font'; import { isGeneral } from '@gitroom/react/helpers/is.general'; @@ -73,6 +73,7 @@ export function RegisterAfter({ }) { const [loading, setLoading] = useState(false); const getQuery = useSearchParams(); + const router = useRouter(); const isAfterProvider = useMemo(() => { return !!token && !!provider; @@ -105,6 +106,12 @@ export function RegisterAfter({ setLoading(false); } + + console.log(register.headers.get('activate'), register.headers.get('Activate')); + + if (register.headers.get('activate')) { + router.push('/auth/activate'); + } }; const rootDomain = useMemo(() => { diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index a3d208a2..1500998f 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -186,6 +186,7 @@ export class OrganizationRepository { role: Role.SUPERADMIN, user: { create: { + activated: body.provider !== 'LOCAL', email: body.email, password: body.password ? AuthService.hashPassword(body.password) diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index be6310e4..350b0bca 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -50,6 +50,7 @@ model User { updatedAt DateTime @updatedAt lastReadNotifications DateTime @default(now()) inviteId String? + activated Boolean @default(true) items ItemUser[] marketplace Boolean @default(true) account String? diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts index 0c2d297c..c51d5283 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts @@ -64,6 +64,17 @@ export class UsersRepository { }); } + activateUser(id: string) { + return this._user.model.user.update({ + where: { + id, + }, + data: { + activated: true, + }, + }); + } + getUserByProvider(providerId: string, provider: Provider) { return this._user.model.user.findFirst({ where: { diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts index 097582c2..e7580632 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts @@ -28,6 +28,10 @@ export class UsersService { return this._usersRepository.getUserByProvider(providerId, provider); } + activateUser(id: string) { + return this._usersRepository.activateUser(id); + } + updatePassword(id: string, password: string) { return this._usersRepository.updatePassword(id, password); } diff --git a/libraries/nestjs-libraries/src/services/email.service.ts b/libraries/nestjs-libraries/src/services/email.service.ts index 96bc94e4..b1e494fe 100644 --- a/libraries/nestjs-libraries/src/services/email.service.ts +++ b/libraries/nestjs-libraries/src/services/email.service.ts @@ -10,8 +10,8 @@ export class EmailService { console.log('No Resend API Key found, skipping email sending'); return; } - await resend.emails.send({ - from: process.env.IS_GENERAL === 'true' ? 'Nevo ' : 'Nevo ', + const sends = await resend.emails.send({ + from: process.env.IS_GENERAL === 'true' ? 'Nevo ' : 'Nevo ', to, subject, html,