diff --git a/back/src/auth/auth.controller.ts b/back/src/auth/auth.controller.ts
index ba91edd..bc48297 100644
--- a/back/src/auth/auth.controller.ts
+++ b/back/src/auth/auth.controller.ts
@@ -18,6 +18,7 @@ import {
HttpStatus,
ParseFilePipeBuilder,
Response,
+ Query,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
@@ -41,7 +42,6 @@ import { SettingsService } from 'src/settings/settings.service';
import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from '@nestjs/platform-express';
import { writeFile } from 'fs';
-import { MailerService } from '@nestjs-modules/mailer';
@ApiTags('auth')
@Controller('auth')
@@ -50,7 +50,6 @@ export class AuthController {
private authService: AuthService,
private usersService: UsersService,
private settingsService: SettingsService,
- private emailService: MailerService,
) {}
@Get('login/google')
@@ -73,19 +72,29 @@ export class AuthController {
try {
const user = await this.usersService.createUser(registerDto);
await this.settingsService.createUserSetting(user.id);
- await this.emailService.sendMail({
- to: user.email,
- from: "chromacase@octohub.app",
- subject: "Mail verification",
- text: "To verify your mail click here",
- html: "Verify",
- })
+ await this.authService.sendVerifyMail(user);
} catch (e) {
console.error(e);
throw new BadRequestException();
}
}
+ @HttpCode(200)
+ @UseGuards(JwtAuthGuard)
+ @Put('verify')
+ async verify(@Request() req: any, @Query('token') token: string): Promise {
+ if (await this.authService.verifyMail(req.user.id, token))
+ return;
+ throw new BadRequestException("Invalid token. Expired or invalid.");
+ }
+
+ @HttpCode(200)
+ @UseGuards(JwtAuthGuard)
+ @Put('reverify')
+ async reverify(@Request() req: any): Promise {
+ await this.authService.sendVerifyMail(req.user);
+ }
+
@ApiBody({ type: LoginDto })
@HttpCode(200)
@UseGuards(LocalAuthGuard)
@@ -130,7 +139,7 @@ export class AuthController {
)
file: Express.Multer.File,
) {
- const path = `/data/${req.user.id}.jpg`
+ const path = `/data/${req.user.id}.jpg`;
writeFile(path, file.buffer, (err) => {
if (err) throw err;
});
diff --git a/back/src/auth/auth.service.ts b/back/src/auth/auth.service.ts
index c733207..56f4679 100644
--- a/back/src/auth/auth.service.ts
+++ b/back/src/auth/auth.service.ts
@@ -1,13 +1,16 @@
-import { BadRequestException, Injectable } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import PayloadInterface from './interface/payload.interface';
+import { User } from 'src/models/user';
+import { MailerService } from '@nestjs-modules/mailer';
@Injectable()
export class AuthService {
constructor(
private userService: UsersService,
private jwtService: JwtService,
+ private emailService: MailerService,
) {}
async validateUser(
@@ -31,4 +34,33 @@ export class AuthService {
access_token,
};
}
+
+ async sendVerifyMail(user: User) {
+ const token = await this.jwtService.signAsync(
+ {
+ userId: user.id,
+ },
+ { expiresIn: '10h' },
+ );
+ await this.emailService.sendMail({
+ to: user.email,
+ from: 'chromacase@octohub.app',
+ subject: 'Mail verification for Chromacase',
+ html: `To verify your mail, please click on this link.`,
+ });
+ }
+
+ async verifyMail(userId: number, token: string): Promise {
+ try {
+ await this.jwtService.verifyAsync(token);
+ } catch(e) {
+ console.log("Verify mail token failure", e);
+ return false;
+ }
+ await this.userService.updateUser({
+ where: { id: userId },
+ data: { emailVerified: true },
+ });
+ return true;
+ }
}
diff --git a/back/src/users/users.service.ts b/back/src/users/users.service.ts
index 55c12b5..62a4a31 100644
--- a/back/src/users/users.service.ts
+++ b/back/src/users/users.service.ts
@@ -1,8 +1,6 @@
import {
Injectable,
InternalServerErrorException,
- NotFoundException,
- StreamableFile,
} from '@nestjs/common';
import { User, Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
@@ -13,7 +11,9 @@ import fetch from 'node-fetch';
@Injectable()
export class UsersService {
- constructor(private prisma: PrismaService) {}
+ constructor(
+ private prisma: PrismaService,
+ ) {}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
@@ -95,8 +95,7 @@ export class UsersService {
const resp = await fetch(
`https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`,
);
- for (const [k, v] of resp.headers)
- resp.headers.set(k, v);
+ for (const [k, v] of resp.headers) resp.headers.set(k, v);
resp.body!.pipe(res);
}
}
diff --git a/front/Navigation.tsx b/front/Navigation.tsx
index 1e12973..486ac99 100644
--- a/front/Navigation.tsx
+++ b/front/Navigation.tsx
@@ -30,6 +30,7 @@ import TextButton from './components/TextButton';
import ErrorView from './views/ErrorView';
import GenreDetailsView from './views/GenreDetailsView';
import GoogleView from './views/GoogleView';
+import VerifiedView from './views/VerifiedView';
// Util function to hide route props in URL
const removeMe = () => '';
@@ -81,6 +82,11 @@ const protectedRoutes = () =>
link: undefined,
},
User: { component: ProfileView, options: { title: translate('user') }, link: '/user' },
+ Verified: {
+ component: VerifiedView,
+ options: { title: 'Verify email', headerShown: false },
+ link: '/verify',
+ },
} as const);
const publicRoutes = () =>
diff --git a/front/models/User.ts b/front/models/User.ts
index 1f85df2..a53a110 100644
--- a/front/models/User.ts
+++ b/front/models/User.ts
@@ -8,6 +8,7 @@ export const UserValidator = yup
username: yup.string().required(),
password: yup.string().required().nullable(),
email: yup.string().required(),
+ emailVerified: yup.boolean().required(),
googleID: yup.string().required().nullable(),
isGuest: yup.boolean().required(),
partyPlayed: yup.number().required(),
@@ -32,6 +33,7 @@ export const UserHandler: ResponseHandler, U
interface User extends Model {
name: string;
email: string;
+ emailVerified: boolean;
googleID: string | null;
isGuest: boolean;
premium: boolean;
diff --git a/front/views/VerifiedView.tsx b/front/views/VerifiedView.tsx
new file mode 100644
index 0000000..37286c7
--- /dev/null
+++ b/front/views/VerifiedView.tsx
@@ -0,0 +1,35 @@
+import { useEffect, useState } from 'react';
+import { useDispatch } from 'react-redux';
+import API from '../API';
+import { Text } from 'native-base';
+import { useNavigation } from '../Navigation';
+import { useRoute } from '@react-navigation/native';
+
+const VerifiedView = () => {
+ const navigation = useNavigation();
+ const route = useRoute();
+ const [failed, setFailed] = useState(false);
+
+ useEffect(() => {
+ async function run() {
+ try {
+ await API.fetch({
+ route: `/auth/verify?token=${(route.params as any).token}`,
+ method: 'PUT',
+ });
+ navigation.navigate('Home');
+ } catch {
+ setFailed(true);
+ }
+ }
+ run();
+ }, []);
+
+ return failed ? (
+ Email verification failed. The token has expired or is invalid.
+ ) : (
+ Loading please wait
+ );
+};
+
+export default VerifiedView;
diff --git a/front/views/settings/SettingsProfileView.tsx b/front/views/settings/SettingsProfileView.tsx
index bf946f4..bd4b199 100644
--- a/front/views/settings/SettingsProfileView.tsx
+++ b/front/views/settings/SettingsProfileView.tsx
@@ -49,7 +49,7 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
type: 'text',
title: translate('email'),
data: {
- text: user.email || translate('NoAssociatedEmail'),
+ text: `${user.email} ${user.emailVerified ? "verified" : "not verified"}` || translate('NoAssociatedEmail'),
onPress: () => {
navigation.navigate('changeEmail');
},