Skip to content

Commit

Permalink
0.0.3 unique user email, unique generation name, ToS and Privacy Poli…
Browse files Browse the repository at this point in the history
…cy link, sentry instrumentation
  • Loading branch information
sayyidyofa committed Jan 13, 2025
1 parent 475c5b4 commit e9bd5fa
Show file tree
Hide file tree
Showing 17 changed files with 148 additions and 39 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ jobs:
JWT_PUBLIC_KEY_BASE64: ${{ secrets.JWT_PUBLIC_KEY_BASE64 }}
PASSWORD_SALT: ${{ secrets.PASSWORD_SALT }}
SUDO_TOKEN: ${{ secrets.SUDO_TOKEN }}
MAILER_API_KEY: ${{ secrets.MAILER_API_KEY }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
APP_ENV_FILE_PATH: ./deploy/env
run: |
echo APP_ENV="$APP_ENV" > "$APP_ENV_FILE_PATH"
Expand All @@ -56,6 +58,8 @@ jobs:
echo JWT_PUBLIC_KEY_BASE64="$JWT_PUBLIC_KEY_BASE64" >> "$APP_ENV_FILE_PATH"
echo PASSWORD_SALT="$PASSWORD_SALT" >> "$APP_ENV_FILE_PATH"
echo SUDO_TOKEN="$SUDO_TOKEN" >> "$APP_ENV_FILE_PATH"
echo MAILER_API_KEY="$MAILER_API_KEY" >> "$APP_ENV_FILE_PATH"
echo SENTRY_DSN="$SENTRY_DSN" >> "$APP_ENV_FILE_PATH"
- name: Sync deploy files Backend
working-directory: ./backend
Expand Down
Binary file modified backend/bun.lockb
Binary file not shown.
7 changes: 5 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ikapiar-backend",
"private": false,
"version": "0.0.2",
"version": "0.0.3",
"description": "IKAPIAR Web Backends",
"license": "MPL-2.0",
"type": "module",
Expand All @@ -10,7 +10,8 @@
"start:dev": "bun --watch ./src/main.ts",
"start:debug": "nest start --debug --watch",
"start:prod": "bun ./src/main.ts",
"typeorm": "typeorm-ts-node-esm"
"typeorm": "typeorm-ts-node-esm",
"migrate:generate": "typeorm migration:generate --dataSource ./typeorm/migration-config.js ./typeorm/migrations/"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
Expand All @@ -34,6 +35,7 @@
"@nestjs/platform-express": "^10.4.15",
"@nestjs/swagger": "^8.1.0",
"@nestjs/typeorm": "^10.0.2",
"@sentry/nestjs": "^8.48.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cookie-parser": "^1.4.7",
Expand All @@ -43,6 +45,7 @@
"passport-local": "^1.0.0",
"passport-openid": "^0.4.0",
"pg": "^8.13.1",
"resend": "^4.0.1",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20",
"valibot": "^1.0.0-beta.9"
Expand Down
8 changes: 7 additions & 1 deletion backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { generateOrmOptions, getConfig } from './common/config';
import { seedDB } from './common/db/seeding.ts';
import { AuthModule } from './modules/auth/auth.module.ts';
import { UserModule } from './modules/user/user.module.ts';
import { APP_GUARD } from '@nestjs/core';
import { APP_FILTER, APP_GUARD } from '@nestjs/core';
import { AuthMiddleware } from './common/middlewares/auth.ts';
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';

@Module({
imports: [
SentryModule.forRoot(),
TypeOrmModule.forRootAsync(generateOrmOptions()),
AuthModule,
UserModule,
Expand All @@ -19,6 +21,10 @@ import { AuthMiddleware } from './common/middlewares/auth.ts';
provide: APP_GUARD,
useClass: AuthMiddleware,
},
{
provide: APP_FILTER,
useClass: SentryGlobalFilter,
},
],
})
export class AppModule implements OnApplicationBootstrap {
Expand Down
12 changes: 12 additions & 0 deletions backend/src/common/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ export const AppConfigs = [
key: 'SUDO_TOKEN',
defaultValue: 'sipalingsudo',
},
{
key: 'MAILER_API_KEY',
defaultValue: 'GANTII',
},
{
key: 'MAILER_FROM',
defaultValue: 'GANTII',
},
{
key: 'SENTRY_DSN',
defaultValue: 'GANTII',
},
{
key: 'PORT',
defaultValue: '4000',
Expand Down
2 changes: 1 addition & 1 deletion backend/src/common/db/entities/Generation.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class Generation {
@PrimaryGeneratedColumn('uuid')
id!: string;

@Column()
@Column({ unique: true })
name!: string;

@Column()
Expand Down
6 changes: 3 additions & 3 deletions backend/src/common/db/entities/User.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ export class User {
@PrimaryGeneratedColumn('uuid')
id!: string;

@Column()
@Column({ unique: true })
email!: string;

@Column({ nullable: true, type: 'text' })
password_hash?: string | undefined; // User's hashed password

@Column({ enum: Roles, type: 'text' })
@Column({ type: 'text' })
roles!: string; // A user can have 0 or more roles

@OneToMany(() => Identity, (identity) => identity.user)
Expand All @@ -46,7 +46,7 @@ export class User {
@Column({ enum: UserStates, type: 'enum' })
state!: UserState;

@CreateDateColumn({ default: new Date() })
@CreateDateColumn()
created_at!: Date; // Record creation timestamp

@UpdateDateColumn()
Expand Down
18 changes: 17 additions & 1 deletion backend/src/common/middlewares/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
type ExceptionFilter,
} from '@nestjs/common';
import type { Response } from 'express';
import { UserNotFound } from '../../modules/user/user.service.ts';
import {
UserAlreadyExist,
UserNotFound,
} from '../../modules/user/user.service.ts';
import { AuthorizationStringEmpty, InvalidJWT } from './auth.ts';

const NotFoundErrors = [UserNotFound] as const;
Expand Down Expand Up @@ -32,3 +35,16 @@ export class AuthorizationExceptionFilter implements ExceptionFilter {
response.status(403).json({ message: exception.toString() });
}
}

const BadRequestErrors = [UserAlreadyExist] as const;
type BadRequestError = (typeof BadRequestErrors)[number];

@Catch(...BadRequestErrors)
export class BadRequestExceptionFilter implements ExceptionFilter {
catch(exception: BadRequestError, host: ArgumentsHost) {
host.switchToHttp()
.getResponse<Response>()
.status(400)
.json({ message: exception.toString() });
}
}
11 changes: 11 additions & 0 deletions backend/src/instrument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Import with `const Sentry = require("@sentry/nestjs");` if you are using CJS
import * as Sentry from '@sentry/nestjs';
import { getConfig } from './common/config';

export function initSentry() {
Sentry.init({
dsn: getConfig('SENTRY_DSN'),
// Tracing
tracesSampleRate: 1.0, // Capture 100% of the transactions
});
}
7 changes: 6 additions & 1 deletion backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { writeFileSync } from 'fs';
import { ValidationPipe } from '@nestjs/common';
import {
AuthorizationExceptionFilter,
BadRequestExceptionFilter,
EntityNotFoundExceptionFilter,
} from './common/middlewares/errors.ts';
import { initSentry } from './instrument.ts';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
Expand All @@ -19,10 +21,13 @@ async function bootstrap() {
app.useGlobalPipes(new ValidationPipe({ transform: true }));
app.useGlobalFilters(
new EntityNotFoundExceptionFilter(),
new AuthorizationExceptionFilter()
new AuthorizationExceptionFilter(),
new BadRequestExceptionFilter()
);
app.use(cookieParser());

initSentry();

// setup swagger
const swaggerOptions = new DocumentBuilder()
.setTitle('Platform API')
Expand Down
21 changes: 21 additions & 0 deletions backend/src/modules/notifier/notifier.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Logger } from '@nestjs/common';
import { Resend } from 'resend';
import { getConfig } from '../../common/config';

export class NotifierService {
private readonly logger = new Logger(NotifierService.name);
private readonly resend = new Resend(getConfig('MAILER_API_KEY'));

sendMail(to: string, subject: string, html: string) {
this.resend.emails
.send({
from: getConfig('MAILER_FROM'),
to,
subject,
html,
})
.catch((e) => {
this.logger.error(`Failed to send email to ${to}: ${e}`);
});
}
}
13 changes: 13 additions & 0 deletions backend/src/modules/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export class UserService {
email: string;
password: string;
}): Promise<string> {
const foundUser = await this.userRepo.findOne({
where: { email: newUser.email },
});
if (foundUser) {
throw new UserAlreadyExist(
`User with email ${newUser.email} already exists`
);
}
const { email, password } = newUser;
const user = new User();
user.email = email;
Expand Down Expand Up @@ -85,3 +93,8 @@ export class UserNotFound extends Error {
super(message);
}
}
export class UserAlreadyExist extends Error {
constructor(message: string) {
super(message);
}
}
2 changes: 1 addition & 1 deletion backend/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
"info": {
"title": "Platform API",
"description": "IKAPIAR Web Backends",
"version": "0.0.1",
"version": "0.0.2",
"contact": {}
},
"tags": [],
Expand Down
2 changes: 1 addition & 1 deletion backend/typeorm/1736686540737-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class Migrations1736686540737 implements MigrationInterface {
await queryRunner.query(`CREATE TYPE "public"."identity_provider_enum" AS ENUM('GOOGLE', 'LINKEDIN')`);
await queryRunner.query(`CREATE TABLE "identity" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "provider" "public"."identity_provider_enum" NOT NULL, "token" character varying NOT NULL, "metadata" jsonb, "expires_at" TIMESTAMP NOT NULL, "userId" uuid, CONSTRAINT "PK_ff16a44186b286d5e626178f726" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TYPE "public"."user_state_enum" AS ENUM('active', 'pending_approval', 'deleted', 'blocked')`);
await queryRunner.query(`CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "password_hash" text, "roles" text NOT NULL, "state" "public"."user_state_enum" NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT '"2025-01-12T12:55:43.023Z"', "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "password_hash" text, "roles" text NOT NULL, "state" "public"."user_state_enum" NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "survey" ("id" SERIAL NOT NULL, "survey_id" character varying NOT NULL, "survey_link" character varying NOT NULL, "participant_email" character varying NOT NULL, "participant_answer" jsonb NOT NULL, CONSTRAINT "PK_f0da32b9181e9c02ecf0be11ed3" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "alumnus" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "graduation_year" integer NOT NULL, "generationId" uuid, CONSTRAINT "UQ_8a556d1ae682e0fa4492a00dab2" UNIQUE ("email"), CONSTRAINT "PK_c04e1638aa67570d77e04eab3e7" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "generation" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "entryYear" integer NOT NULL, CONSTRAINT "PK_58db1b8155c99c2604394ffef2a" PRIMARY KEY ("id"))`);
Expand Down
16 changes: 16 additions & 0 deletions backend/typeorm/1736753250218-migrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class Migrations1736753250218 implements MigrationInterface {
name = 'Migrations1736753250218'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email")`);
await queryRunner.query(`ALTER TABLE "generation" ADD CONSTRAINT "UQ_97ea6861407ef35fd5dc13ad75e" UNIQUE ("name")`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "generation" DROP CONSTRAINT "UQ_97ea6861407ef35fd5dc13ad75e"`);
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22"`);
}

}
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ikapiar-frontend",
"version": "0.0.2",
"version": "0.0.3",
"private": false,
"scripts": {
"dev": "next dev",
Expand Down
56 changes: 29 additions & 27 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,50 +51,52 @@ export default function Home() {
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
<a href="https://assets.ikapiar.my.id/LEGAL.md">Privacy Policy</a>
<a href="https://assets.ikapiar.my.id/LEGAL.md">Terms of Service</a>
</footer>
</div>
);
Expand Down

0 comments on commit e9bd5fa

Please sign in to comment.