Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ REFRESH_TOKEN_EXPIRES_IN=7d
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/vibeline
CORS_ORIGIN=http://localhost:3000,http://localhost:3002
APP_URL=http://localhost:3002
DEMO_SEED_PASSWORD=Demo123!

# SMTP (required for email verification/password reset flows)
SMTP_HOST=smtp.example.com
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# dependencies
node_modules

desigining-flow.md
coding-practices.md
# env
.env
.env.*
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,9 @@ Web app runs on `http://localhost:3002` and API on `http://localhost:5001`.
- `pnpm lint` lint all workspaces
- `pnpm typecheck` strict typecheck
- `pnpm build` production builds
- `pnpm validate` run lint + typecheck + build in sequence

More detail: `docs/architecture.md`
More detail:

- `docs/architecture.md`
- `docs/engineering-standards.md`
5 changes: 5 additions & 0 deletions apps/server/drizzle/0003_add_users_lookup_indexes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE INDEX IF NOT EXISTS "users_created_at_idx" ON "users" ("created_at");
CREATE INDEX IF NOT EXISTS "users_verification_token_idx" ON "users" ("verification_token");
CREATE INDEX IF NOT EXISTS "users_verification_code_idx" ON "users" ("verification_code");
CREATE INDEX IF NOT EXISTS "users_password_reset_token_idx" ON "users" ("password_reset_token");
CREATE INDEX IF NOT EXISTS "users_password_reset_code_idx" ON "users" ("password_reset_code");
7 changes: 7 additions & 0 deletions apps/server/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
"when": 1771621438507,
"tag": "0002_add_password_reset_code",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1771780000000,
"tag": "0003_add_users_lookup_indexes",
"breakpoints": true
}
]
}
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"start": "node dist/server.cjs",
"lint": "eslint src --ext .ts",
"typecheck": "tsc --noEmit",
"test": "node --import tsx --test $(find tests -name '*.test.ts')",
"db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/db/migrate.ts",
"db:seed": "tsx src/db/seed.ts"
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const envSchema = z.object({
.default('false'),
INVITE_FROM_EMAIL: z.string().email(),

APP_URL: z.string().url().default('http://localhost:3000'),
APP_URL: z.string().url().default('http://localhost:3002'),

// Google OAuth
GOOGLE_CLIENT_ID: z.string().optional(),
Expand Down
44 changes: 17 additions & 27 deletions apps/server/src/config/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,21 @@ import type { FastifyServerOptions } from 'fastify';

import { env } from '@/config/env';

export const loggerConfig: FastifyServerOptions['logger'] =
env.NODE_ENV === 'development'
? {
transport: {
target: 'pino-pretty',
options: {
singleLine: true,
colorize: true,
translateTime: 'SYS:standard'
}
}
}
: true;
const prettyTransport = {
transport: {
target: 'pino-pretty',
options: {
singleLine: true,
colorize: true,
translateTime: 'SYS:standard'
}
}
} as const;

export const logger = pino(
env.NODE_ENV === 'development'
? {
transport: {
target: 'pino-pretty',
options: {
singleLine: true,
colorize: true,
translateTime: 'SYS:standard'
}
}
}
: {}
);
const usePrettyLogger = env.NODE_ENV === 'development';

export const loggerConfig: FastifyServerOptions['logger'] = usePrettyLogger
? prettyTransport
: true;

export const logger = pino(usePrettyLogger ? prettyTransport : {});
57 changes: 31 additions & 26 deletions apps/server/src/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import {
boolean,
pgTable,
text,
timestamp
} from 'drizzle-orm/pg-core';
import { boolean, index, pgTable, text, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
displayName: text('display_name').notNull(),
avatarUrl: text('avatar_url'),
role: text('role').notNull().default('user'),
emailVerified: boolean('email_verified').notNull().default(false),
passwordHash: text('password_hash').notNull(),
verificationToken: text('verification_token'),
verificationCode: text('verification_code'),
verificationTokenExpiresAt: timestamp('verification_token_expires_at', {
withTimezone: true
}),
passwordResetToken: text('password_reset_token'),
passwordResetCode: text('password_reset_code'),
passwordResetTokenExpiresAt: timestamp('password_reset_token_expires_at', {
withTimezone: true
}),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow()
});
export const users = pgTable(
'users',
{
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
displayName: text('display_name').notNull(),
avatarUrl: text('avatar_url'),
role: text('role').notNull().default('user'),
emailVerified: boolean('email_verified').notNull().default(false),
passwordHash: text('password_hash').notNull(),
verificationToken: text('verification_token'),
verificationCode: text('verification_code'),
verificationTokenExpiresAt: timestamp('verification_token_expires_at', {
withTimezone: true
}),
passwordResetToken: text('password_reset_token'),
passwordResetCode: text('password_reset_code'),
passwordResetTokenExpiresAt: timestamp('password_reset_token_expires_at', {
withTimezone: true
}),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow()
},
(table) => ({
createdAtIdx: index('users_created_at_idx').on(table.createdAt),
verificationTokenIdx: index('users_verification_token_idx').on(table.verificationToken),
verificationCodeIdx: index('users_verification_code_idx').on(table.verificationCode),
passwordResetTokenIdx: index('users_password_reset_token_idx').on(table.passwordResetToken),
passwordResetCodeIdx: index('users_password_reset_code_idx').on(table.passwordResetCode)
})
);
24 changes: 12 additions & 12 deletions apps/server/src/db/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,28 @@ import { eq } from 'drizzle-orm';
import { db } from './client';
import { users } from './schema';

const ADMIN_PASSWORD = process.env.ADMIN_SEED_PASSWORD ?? 'Admin123!';
const DEMO_PASSWORD = process.env.DEMO_SEED_PASSWORD ?? 'Demo123!';
const DEMO_EMAIL = 'demo@vibeline.dev';

async function seed() {
const adminExists = await db.query.users.findFirst({
where: eq(users.email, 'admin@vibeline.dev')
const demoUserExists = await db.query.users.findFirst({
where: eq(users.email, DEMO_EMAIL)
});

if (!adminExists) {
const passwordHash = await hash(ADMIN_PASSWORD, 12);
if (!demoUserExists) {
const passwordHash = await hash(DEMO_PASSWORD, 12);
await db.insert(users).values({
id: 'u_admin',
email: 'admin@vibeline.dev',
displayName: 'Workspace Admin',
role: 'admin',
id: 'u_demo',
email: DEMO_EMAIL,
displayName: 'Demo User',
role: 'user',
emailVerified: true,
passwordHash
});
console.log('Created admin user (admin@vibeline.dev)');
console.log(`Created demo user (${DEMO_EMAIL})`);
} else {
console.log('Admin user already exists');
console.log('Demo user already exists');
}

}

seed()
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/middleware/error.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { env } from '@/config/env';

const allowedOrigins = env.CORS_ORIGIN.split(',').map((o) => o.trim());

const setCorsHeaders = (request: { headers: { origin?: string } }, reply: { header: (name: string, value: string) => void }) => {
const setCorsHeaders = (
request: { headers: { origin?: string } },
reply: { header: (name: string, value: string) => void }
) => {
const origin = request.headers.origin;
if (origin && allowedOrigins.includes(origin)) {
reply.header('Access-Control-Allow-Origin', origin);
Expand Down
28 changes: 28 additions & 0 deletions apps/server/src/modules/auth/auth.artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { randomBytes } from 'node:crypto';

const SIX_DIGIT_UPPER_BOUND = 1_000_000;

const generateHexToken = () => randomBytes(32).toString('hex');

const generateSixDigitCode = () => {
const value = randomBytes(4).readUInt32BE(0) % SIX_DIGIT_UPPER_BOUND;
return value.toString().padStart(6, '0');
};

const generateExpiryTimestamp = (hours: number) => {
const expiry = new Date();
expiry.setHours(expiry.getHours() + hours);
return expiry.toISOString();
};

export const createVerificationArtifacts = () => ({
verificationToken: generateHexToken(),
verificationCode: generateSixDigitCode(),
verificationTokenExpiresAt: generateExpiryTimestamp(24)
});

export const createPasswordResetArtifacts = () => ({
passwordResetToken: generateHexToken(),
passwordResetCode: generateSixDigitCode(),
passwordResetTokenExpiresAt: generateExpiryTimestamp(1)
});
Loading