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,