mirror of
https://github.com/swissmakers/swiss-datashare.git
synced 2026-04-11 10:27:01 +02:00
feat: remove appwrite and add nextjs backend
This commit is contained in:
42
backend/src/auth/auth.controller.ts
Normal file
42
backend/src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
ForbiddenException,
|
||||
HttpCode,
|
||||
Post,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { AuthService } from "./auth.service";
|
||||
import { AuthDTO } from "./dto/auth.dto";
|
||||
import { AuthRegisterDTO } from "./dto/authRegister.dto";
|
||||
import { RefreshAccessTokenDTO } from "./dto/refreshAccessToken.dto";
|
||||
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private config: ConfigService
|
||||
) {}
|
||||
|
||||
@Post("signUp")
|
||||
signUp(@Body() dto: AuthRegisterDTO) {
|
||||
if (!this.config.get("ALLOW_REGISTRATION"))
|
||||
throw new ForbiddenException("Registration is not allowed");
|
||||
return this.authService.signUp(dto);
|
||||
}
|
||||
|
||||
@Post("signIn")
|
||||
signIn(@Body() dto: AuthDTO) {
|
||||
return this.authService.signIn(dto);
|
||||
}
|
||||
|
||||
@Post("token")
|
||||
@HttpCode(200)
|
||||
async refreshAccessToken(@Body() body: RefreshAccessTokenDTO) {
|
||||
const accessToken = await this.authService.refreshAccessToken(
|
||||
body.refreshToken
|
||||
);
|
||||
return { accessToken };
|
||||
}
|
||||
}
|
||||
13
backend/src/auth/auth.module.ts
Normal file
13
backend/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtStrategy } from "./strategy/jwt.strategy";
|
||||
|
||||
@Module({
|
||||
imports: [JwtModule.register({})],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
95
backend/src/auth/auth.service.ts
Normal file
95
backend/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||
import * as argon from "argon2";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { AuthDTO } from "./dto/auth.dto";
|
||||
import { AuthRegisterDTO } from "./dto/authRegister.dto";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private jwtService: JwtService,
|
||||
private config: ConfigService
|
||||
) {}
|
||||
|
||||
async signUp(dto: AuthRegisterDTO) {
|
||||
const hash = await argon.hash(dto.password);
|
||||
try {
|
||||
const user = await this.prisma.user.create({
|
||||
data: {
|
||||
email: dto.email,
|
||||
password: hash,
|
||||
},
|
||||
});
|
||||
|
||||
const accessToken = await this.createAccessToken(user);
|
||||
const refreshToken = await this.createRefreshToken(user.id);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e.code == "P2002") {
|
||||
throw new BadRequestException("Credentials taken");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async signIn(dto: AuthDTO) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: {
|
||||
email: dto.email,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !(await argon.verify(user.password, dto.password)))
|
||||
throw new UnauthorizedException("Wrong email or password");
|
||||
|
||||
const accessToken = await this.createAccessToken(user);
|
||||
const refreshToken = await this.createRefreshToken(user.id);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
async createAccessToken(user: User) {
|
||||
return this.jwtService.sign(
|
||||
{
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
},
|
||||
{
|
||||
expiresIn: "15min",
|
||||
secret: this.config.get("JWT_SECRET"),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async refreshAccessToken(refreshToken: string) {
|
||||
const refreshTokenMetaData = await this.prisma.refreshToken.findUnique({
|
||||
where: { token: refreshToken },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (!refreshTokenMetaData || refreshTokenMetaData.expiresAt < new Date())
|
||||
throw new UnauthorizedException();
|
||||
|
||||
return this.createAccessToken(refreshTokenMetaData.user);
|
||||
}
|
||||
|
||||
async createRefreshToken(userId: string) {
|
||||
const refreshToken = (
|
||||
await this.prisma.refreshToken.create({ data: { userId } })
|
||||
).token;
|
||||
|
||||
return refreshToken;
|
||||
}
|
||||
}
|
||||
9
backend/src/auth/decorator/getUser.decorator.ts
Normal file
9
backend/src/auth/decorator/getUser.decorator.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
||||
|
||||
export const GetUser = createParamDecorator(
|
||||
(data: string, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
return data ? user?.[data] : user;
|
||||
}
|
||||
);
|
||||
26
backend/src/auth/dto/auth.dto.ts
Normal file
26
backend/src/auth/dto/auth.dto.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Expose, plainToClass } from "class-transformer";
|
||||
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
export class AuthDTO {
|
||||
@Expose()
|
||||
id: string;
|
||||
|
||||
@Expose()
|
||||
firstName: string;
|
||||
|
||||
@Expose()
|
||||
lastName: string;
|
||||
|
||||
@Expose()
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
constructor(partial: Partial<AuthDTO>) {
|
||||
return plainToClass(AuthDTO, partial, { excludeExtraneousValues: true });
|
||||
}
|
||||
}
|
||||
4
backend/src/auth/dto/authRegister.dto.ts
Normal file
4
backend/src/auth/dto/authRegister.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { AuthDTO } from "./auth.dto";
|
||||
|
||||
export class AuthRegisterDTO extends AuthDTO {}
|
||||
7
backend/src/auth/dto/authSignIn.dto.ts
Normal file
7
backend/src/auth/dto/authSignIn.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { AuthDTO } from "./auth.dto";
|
||||
|
||||
export class AuthSignInDTO extends PickType(AuthDTO, [
|
||||
"email",
|
||||
"password",
|
||||
] as const) {}
|
||||
6
backend/src/auth/dto/refreshAccessToken.dto.ts
Normal file
6
backend/src/auth/dto/refreshAccessToken.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
export class RefreshAccessTokenDTO {
|
||||
@IsNotEmpty()
|
||||
refreshToken: string;
|
||||
}
|
||||
7
backend/src/auth/guard/jwt.guard.ts
Normal file
7
backend/src/auth/guard/jwt.guard.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
export class JwtGuard extends AuthGuard("jwt") {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
37
backend/src/auth/jobs/jobs.service.ts
Normal file
37
backend/src/auth/jobs/jobs.service.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Cron } from "@nestjs/schedule";
|
||||
import { FileService } from "src/file/file.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class JobsService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private fileService: FileService
|
||||
) {}
|
||||
|
||||
@Cron("0 * * * *")
|
||||
async deleteExpiredShares() {
|
||||
const expiredShares = await this.prisma.share.findMany({
|
||||
where: { expiration: { lt: new Date() } },
|
||||
});
|
||||
|
||||
for (const expiredShare of expiredShares) {
|
||||
await this.prisma.share.delete({
|
||||
where: { id: expiredShare.id },
|
||||
});
|
||||
|
||||
await this.fileService.deleteAllFiles(expiredShare.id);
|
||||
}
|
||||
|
||||
console.log(`job: deleted ${expiredShares.length} expired shares`);
|
||||
}
|
||||
|
||||
@Cron("0 * * * *")
|
||||
async deleteExpiredRefreshTokens() {
|
||||
const expiredShares = await this.prisma.refreshToken.deleteMany({
|
||||
where: { expiresAt: { lt: new Date() } },
|
||||
});
|
||||
console.log(`job: deleted ${expiredShares.count} expired refresh tokens`);
|
||||
}
|
||||
}
|
||||
24
backend/src/auth/strategy/jwt.strategy.ts
Normal file
24
backend/src/auth/strategy/jwt.strategy.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { User } from "@prisma/client";
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(config: ConfigService, private prisma: PrismaService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: config.get("JWT_SECRET"),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any) {
|
||||
const user: User = await this.prisma.user.findUnique({
|
||||
where: { id: payload.sub },
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user