Add verified badge and page on the front

This commit is contained in:
2023-09-13 17:25:01 +02:00
parent 3b2ca9963b
commit 5c83235cba
7 changed files with 100 additions and 17 deletions
+19 -10
View File
@@ -18,6 +18,7 @@ import {
HttpStatus, HttpStatus,
ParseFilePipeBuilder, ParseFilePipeBuilder,
Response, Response,
Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard'; import { JwtAuthGuard } from './jwt-auth.guard';
@@ -41,7 +42,6 @@ import { SettingsService } from 'src/settings/settings.service';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { writeFile } from 'fs'; import { writeFile } from 'fs';
import { MailerService } from '@nestjs-modules/mailer';
@ApiTags('auth') @ApiTags('auth')
@Controller('auth') @Controller('auth')
@@ -50,7 +50,6 @@ export class AuthController {
private authService: AuthService, private authService: AuthService,
private usersService: UsersService, private usersService: UsersService,
private settingsService: SettingsService, private settingsService: SettingsService,
private emailService: MailerService,
) {} ) {}
@Get('login/google') @Get('login/google')
@@ -73,19 +72,29 @@ export class AuthController {
try { try {
const user = await this.usersService.createUser(registerDto); const user = await this.usersService.createUser(registerDto);
await this.settingsService.createUserSetting(user.id); await this.settingsService.createUserSetting(user.id);
await this.emailService.sendMail({ await this.authService.sendVerifyMail(user);
to: user.email,
from: "chromacase@octohub.app",
subject: "Mail verification",
text: "To verify your mail click here",
html: "<b>Verify</b>",
})
} catch (e) { } catch (e) {
console.error(e); console.error(e);
throw new BadRequestException(); throw new BadRequestException();
} }
} }
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@Put('verify')
async verify(@Request() req: any, @Query('token') token: string): Promise<void> {
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<void> {
await this.authService.sendVerifyMail(req.user);
}
@ApiBody({ type: LoginDto }) @ApiBody({ type: LoginDto })
@HttpCode(200) @HttpCode(200)
@UseGuards(LocalAuthGuard) @UseGuards(LocalAuthGuard)
@@ -130,7 +139,7 @@ export class AuthController {
) )
file: Express.Multer.File, file: Express.Multer.File,
) { ) {
const path = `/data/${req.user.id}.jpg` const path = `/data/${req.user.id}.jpg`;
writeFile(path, file.buffer, (err) => { writeFile(path, file.buffer, (err) => {
if (err) throw err; if (err) throw err;
}); });
+33 -1
View File
@@ -1,13 +1,16 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service'; import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs'; import * as bcrypt from 'bcryptjs';
import PayloadInterface from './interface/payload.interface'; import PayloadInterface from './interface/payload.interface';
import { User } from 'src/models/user';
import { MailerService } from '@nestjs-modules/mailer';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor( constructor(
private userService: UsersService, private userService: UsersService,
private jwtService: JwtService, private jwtService: JwtService,
private emailService: MailerService,
) {} ) {}
async validateUser( async validateUser(
@@ -31,4 +34,33 @@ export class AuthService {
access_token, 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 <a href="{${process.env.PUBLIC_URL}/verify?token=${token}">link</a>.`,
});
}
async verifyMail(userId: number, token: string): Promise<boolean> {
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;
}
} }
+4 -5
View File
@@ -1,8 +1,6 @@
import { import {
Injectable, Injectable,
InternalServerErrorException, InternalServerErrorException,
NotFoundException,
StreamableFile,
} from '@nestjs/common'; } from '@nestjs/common';
import { User, Prisma } from '@prisma/client'; import { User, Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
@@ -13,7 +11,9 @@ import fetch from 'node-fetch';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor(private prisma: PrismaService) {} constructor(
private prisma: PrismaService,
) {}
async user( async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput, userWhereUniqueInput: Prisma.UserWhereUniqueInput,
@@ -95,8 +95,7 @@ export class UsersService {
const resp = await fetch( const resp = await fetch(
`https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`, `https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`,
); );
for (const [k, v] of resp.headers) for (const [k, v] of resp.headers) resp.headers.set(k, v);
resp.headers.set(k, v);
resp.body!.pipe(res); resp.body!.pipe(res);
} }
} }
+6
View File
@@ -29,6 +29,7 @@ import { unsetAccessToken } from './state/UserSlice';
import TextButton from './components/TextButton'; import TextButton from './components/TextButton';
import ErrorView from './views/ErrorView'; import ErrorView from './views/ErrorView';
import GoogleView from './views/GoogleView'; import GoogleView from './views/GoogleView';
import VerifiedView from './views/VerifiedView';
// Util function to hide route props in URL // Util function to hide route props in URL
const removeMe = () => ''; const removeMe = () => '';
@@ -75,6 +76,11 @@ const protectedRoutes = () =>
link: undefined, link: undefined,
}, },
User: { component: ProfileView, options: { title: translate('user') }, link: '/user' }, User: { component: ProfileView, options: { title: translate('user') }, link: '/user' },
Verified: {
component: VerifiedView,
options: { title: 'Verify email', headerShown: false },
link: '/verify',
},
} as const); } as const);
const publicRoutes = () => const publicRoutes = () =>
+2
View File
@@ -8,6 +8,7 @@ export const UserValidator = yup
username: yup.string().required(), username: yup.string().required(),
password: yup.string().required().nullable(), password: yup.string().required().nullable(),
email: yup.string().required(), email: yup.string().required(),
emailVerified: yup.boolean().required(),
googleID: yup.string().required().nullable(), googleID: yup.string().required().nullable(),
isGuest: yup.boolean().required(), isGuest: yup.boolean().required(),
partyPlayed: yup.number().required(), partyPlayed: yup.number().required(),
@@ -32,6 +33,7 @@ export const UserHandler: ResponseHandler<yup.InferType<typeof UserValidator>, U
interface User extends Model { interface User extends Model {
name: string; name: string;
email: string; email: string;
emailVerified: boolean;
googleID: string | null; googleID: string | null;
isGuest: boolean; isGuest: boolean;
premium: boolean; premium: boolean;
+35
View File
@@ -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 ? (
<Text>Email verification failed. The token has expired or is invalid.</Text>
) : (
<Text>Loading please wait</Text>
);
};
export default VerifiedView;
+1 -1
View File
@@ -49,7 +49,7 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
type: 'text', type: 'text',
title: translate('email'), title: translate('email'),
data: { data: {
text: user.email || translate('NoAssociatedEmail'), text: `${user.email} ${user.emailVerified ? "verified" : "not verified"}` || translate('NoAssociatedEmail'),
onPress: () => { onPress: () => {
navigation.navigate('changeEmail'); navigation.navigate('changeEmail');
}, },