6 Commits

Author SHA1 Message Date
GitBluub
10f033fe78 wip: password reset routes 2023-09-15 15:11:14 +02:00
869b2e696f Update .env.example 2023-09-13 17:32:10 +02:00
050c970e7e Add a button to resend verified mail 2023-09-13 17:30:34 +02:00
5c83235cba Add verified badge and page on the front 2023-09-13 17:25:01 +02:00
3b2ca9963b Use a fixed python version for the scorometer 2023-09-11 16:13:28 +02:00
0a08193418 Send mails on account creation 2023-09-07 16:58:18 +02:00
20 changed files with 4975 additions and 273 deletions

View File

@@ -10,3 +10,5 @@ SCORO_URL=ws://localhost:6543
GOOGLE_CLIENT_ID=toto GOOGLE_CLIENT_ID=toto
GOOGLE_SECRET=tata GOOGLE_SECRET=tata
GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google
SMTP_TRANSPORT=
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'

5
.envrc
View File

@@ -1,4 +1 @@
if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then use nix
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
fi
use flake

4994
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/common": "^10.1.0", "@nestjs/common": "^10.1.0",
"@nestjs/config": "^3.0.0", "@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.1.0", "@nestjs/core": "^10.1.0",
@@ -37,6 +38,7 @@
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"node-fetch": "^2.6.12", "node-fetch": "^2.6.12",
"nodemailer": "^6.9.5",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
@@ -53,6 +55,7 @@
"@types/jest": "29.5.3", "@types/jest": "29.5.3",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^20.4.4", "@types/node": "^20.4.4",
"@types/nodemailer": "^6.4.9",
"@types/passport-google-oauth20": "^2.0.11", "@types/passport-google-oauth20": "^2.0.11",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/eslint-plugin": "^6.1.0",

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "emailVerified" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -14,6 +14,7 @@ model User {
username String @unique username String @unique
password String? password String?
email String email String
emailVerified Boolean @default(false)
googleID String? @unique googleID String? @unique
isGuest Boolean @default(false) isGuest Boolean @default(false)
partyPlayed Int @default(0) partyPlayed Int @default(0)

View File

@@ -14,6 +14,7 @@ import { ArtistModule } from './artist/artist.module';
import { AlbumModule } from './album/album.module'; import { AlbumModule } from './album/album.module';
import { SearchModule } from './search/search.module'; import { SearchModule } from './search/search.module';
import { HistoryModule } from './history/history.module'; import { HistoryModule } from './history/history.module';
import { MailerModule } from '@nestjs-modules/mailer';
@Module({ @Module({
imports: [ imports: [
@@ -28,6 +29,12 @@ import { HistoryModule } from './history/history.module';
SearchModule, SearchModule,
SettingsModule, SettingsModule,
HistoryModule, HistoryModule,
MailerModule.forRoot({
transport: process.env.SMTP_TRANSPORT,
defaults: {
from: process.env.MAIL_AUTHOR,
},
}),
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService, PrismaService, ArtistService], providers: [AppService, PrismaService, ArtistService],

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';
@@ -71,12 +72,45 @@ 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.authService.sendVerifyMail(user);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
throw new BadRequestException(); throw new BadRequestException();
} }
} }
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@Put('reset')
async password_reset(@Request() req: any, @Query('token') token: string): Promise<void> {
if (await this.authService.resetPassword(req.user.id, token))
return;
throw new BadRequestException("Invalid token. Expired or invalid.");
}
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@Put('send-reset')
async send_reset(@Request() req: any): Promise<void> {
await this.authService.sendResetMail(req.user);
}
@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)
@@ -121,7 +155,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;
}); });

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

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

43
flake.lock generated
View File

@@ -1,43 +0,0 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1665573177,
"narHash": "sha256-Arkrf3zmi3lXYpbSe9H+HQxswQ6jxsAmeQVq5Sr/OZc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d2afb051ffd904af5a825f58abee3c63b148c5f2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "master",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,31 +0,0 @@
{
description = "A prisma test project";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/master";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShell = pkgs.mkShell {
nativeBuildInputs = [ pkgs.bashInteractive ];
buildInputs = with pkgs; [
nodePackages.prisma
nodePackages."@nestjs/cli"
nodePackages.npm
nodejs-slim
yarn
python3
pkg-config
];
shellHook = with pkgs; ''
export PRISMA_MIGRATION_ENGINE_BINARY="${prisma-engines}/bin/migration-engine"
export PRISMA_QUERY_ENGINE_BINARY="${prisma-engines}/bin/query-engine"
export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node"
export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine"
export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt"
export DATABASE_URL=postgresql://user:eip@localhost:5432/chromacase
'';
};
});
}

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

View File

@@ -182,6 +182,7 @@ export const en = {
noRecentSearches: 'No recent searches', noRecentSearches: 'No recent searches',
avatar: 'Avatar', avatar: 'Avatar',
changeIt: 'Change It', changeIt: 'Change It',
verified: "Verified",
}; };
export const fr: typeof en = { export const fr: typeof en = {
@@ -366,6 +367,7 @@ export const fr: typeof en = {
noRecentSearches: 'Aucune recherche récente', noRecentSearches: 'Aucune recherche récente',
avatar: 'Avatar', avatar: 'Avatar',
changeIt: 'Modifier', changeIt: 'Modifier',
verified: "Verifié",
}; };
export const sp: typeof en = { export const sp: typeof en = {
@@ -555,4 +557,5 @@ export const sp: typeof en = {
avatar: 'Avatar', avatar: 'Avatar',
changeIt: 'Cambialo', changeIt: 'Cambialo',
verified: "Verified"
}; };

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;

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;

View File

@@ -55,6 +55,16 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
}, },
}, },
}, },
{
type: 'text',
title: translate('verified'),
data: {
text: user.emailVerified ? 'verified' : 'not verified',
onPress: user.emailVerified
? undefined
: () => API.fetch({ route: '/auth/reverify', method: 'PUT' }),
},
},
{ {
type: 'text', type: 'text',
title: translate('avatar'), title: translate('avatar'),

View File

@@ -1,4 +1,4 @@
FROM python:latest FROM python:3.10
RUN wget -q -O /tmp/websocketd.zip \ RUN wget -q -O /tmp/websocketd.zip \
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \ https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \ && unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \

View File

@@ -1,4 +1,4 @@
FROM python:latest FROM python:3.10
RUN wget -q -O /tmp/websocketd.zip \ RUN wget -q -O /tmp/websocketd.zip \
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \ https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \ && unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \

21
shell.nix Normal file
View File

@@ -0,0 +1,21 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
nativeBuildInputs = [pkgs.bashInteractive];
buildInputs = with pkgs; [
nodePackages.prisma
nodePackages."@nestjs/cli"
nodePackages.npm
nodejs_16
yarn
python3
pkg-config
];
shellHook = with pkgs; ''
# export PRISMA_MIGRATION_ENGINE_BINARY="${prisma-engines}/bin/migration-engine"
# export PRISMA_QUERY_ENGINE_BINARY="${prisma-engines}/bin/query-engine"
export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node"
export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine"
export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt"
export DATABASE_URL=postgresql://user:eip@localhost:5432/chromacase
'';
}