feat: activate your email

This commit is contained in:
Nevo David 2024-06-05 14:46:33 +07:00
parent 498d4ef3c4
commit e534476189
15 changed files with 184 additions and 12 deletions

View File

@ -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,

View File

@ -14,7 +14,7 @@ async function bootstrap() {
rawBody: true,
cors: {
credentials: true,
exposedHeaders: ['reload', 'onboarding'],
exposedHeaders: ['reload', 'onboarding', 'activate'],
origin: [process.env.FRONTEND_URL],
}
});

View File

@ -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) {

View File

@ -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 <a href="${process.env.FRONTEND_URL}/auth/activate/${obj.jwt}">here</a> 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

View File

@ -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 <AfterActivate />;
}

View File

@ -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 (
<Activate />
);
}

View File

@ -0,0 +1,16 @@
'use client';
export function Activate() {
return (
<>
<div>
<h1 className="text-3xl font-bold text-left mb-4 cursor-pointer">
Activate your account
</h1>
</div>
<div className="text-white">
Thank you for registering!<br />Please check your email to activate your account.
</div>
</>
);
}

View File

@ -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 ? <LoadingComponent /> : (<>This user is already activated,<br /><Link href="/auth/login" className="underline">Click here to go back to login</Link></>)}</>
);
};

View File

@ -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);

View File

@ -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(() => {

View File

@ -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)

View File

@ -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?

View File

@ -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: {

View File

@ -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);
}

View File

@ -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@postiz.com>' : 'Nevo <nevo@gitroom.com>',
const sends = await resend.emails.send({
from: process.env.IS_GENERAL === 'true' ? 'Nevo <nevo@gitroom.com>' : 'Nevo <nevo@gitroom.com>',
to,
subject,
html,