Onboarding

This commit is contained in:
Bastian Wagner
2026-06-09 13:07:07 +02:00
parent 537c7cbbee
commit 8f3806d398
29 changed files with 1037 additions and 26 deletions

View File

@@ -3,6 +3,8 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
"deleteOutDir": true,
"webpack": true,
"webpackConfigPath": "webpack.config.js"
}
}

View File

@@ -4,13 +4,18 @@ import {
Get,
HttpCode,
HttpStatus,
Patch,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import type { AuthenticatedRequest } from './auth.types';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { RegisterDto } from './dto/register.dto';
import { JwtAuthGuard } from './jwt-auth.guard';
@Controller('auth')
export class AuthController {
@@ -36,4 +41,22 @@ export class AuthController {
refresh(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refresh(refreshTokenDto);
}
@Get('me')
@UseGuards(JwtAuthGuard)
me(@Req() request: AuthenticatedRequest) {
return this.authService.getPublicUser(request.user!.sub);
}
@Patch('me/onboarding')
@UseGuards(JwtAuthGuard)
updateOnboarding(
@Req() request: AuthenticatedRequest,
@Body() body: { completed?: boolean },
) {
return this.authService.updateOnboardingCompleted(
request.user!.sub,
body.completed === true,
);
}
}

View File

@@ -193,6 +193,35 @@ export class AuthService {
return user.name || user.email;
}
async getPublicUser(userId: string): Promise<PublicUser> {
const user = await this.usersRepository.findOne({
where: { id: userId },
});
if (!user) {
throw new UnauthorizedException('Authenticated user is required.');
}
return this.toPublicUser(user);
}
async updateOnboardingCompleted(
userId: string,
completed: boolean,
): Promise<PublicUser> {
const user = await this.usersRepository.findOne({
where: { id: userId },
});
if (!user) {
throw new UnauthorizedException('Authenticated user is required.');
}
user.onboardingCompleted = completed;
return this.toPublicUser(await this.usersRepository.save(user));
}
private normalizeEmail(email?: string): string {
const normalizedEmail = email?.trim().toLowerCase();
@@ -328,6 +357,7 @@ export class AuthService {
email: user.email,
name: user.name ?? undefined,
verified: user.verified,
onboardingCompleted: user.onboardingCompleted === true,
};
}
}

View File

@@ -7,6 +7,7 @@ export interface AuthUser {
passwordHash: string;
verificationToken?: string;
verified: boolean;
onboardingCompleted: boolean;
}
export interface AuthTokens {
@@ -34,4 +35,5 @@ export interface PublicUser {
email: string;
name?: string;
verified: boolean;
onboardingCompleted: boolean;
}

View File

@@ -33,6 +33,9 @@ export class UserEntity {
@Column({ type: 'boolean', default: false })
verified!: boolean;
@Column({ type: 'boolean', default: false })
onboardingCompleted!: boolean;
@CreateDateColumn({
type: 'datetime',
precision: 3,

View File

@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserOnboardingCompleted1781000000000
implements MigrationInterface
{
name = 'AddUserOnboardingCompleted1781000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
if (!(await queryRunner.hasTable('users'))) {
return;
}
if (!(await queryRunner.hasColumn('users', 'onboardingCompleted'))) {
await queryRunner.query(
'ALTER TABLE `users` ADD `onboardingCompleted` tinyint NOT NULL DEFAULT 0',
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
if (
(await queryRunner.hasTable('users')) &&
(await queryRunner.hasColumn('users', 'onboardingCompleted'))
) {
await queryRunner.query(
'ALTER TABLE `users` DROP COLUMN `onboardingCompleted`',
);
}
}
}

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class GeneratedMigration1781003163444 implements MigrationInterface {
name = 'GeneratedMigration1781003163444'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`users\` ADD \`onboardingCompleted\` tinyint NOT NULL DEFAULT 0`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`users\` DROP COLUMN \`onboardingCompleted\``);
}
}

View File

@@ -0,0 +1,4 @@
module.exports = (options) => ({
...options,
externals: [],
});