Skip to content
Merged
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
126 changes: 126 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"migrate:rollback": "knex migrate:rollback",
"seed": "knex seed:run",
"lint": "eslint src --ext .ts",
"format": "prettier --write \"src/**/*.ts\""
"format": "prettier --write \"src/**/*.ts\"",
"delete-user": "ts-node src/scripts/delete-supabase-user.ts"
},
"keywords": [
"music",
Expand All @@ -24,6 +25,7 @@
"dependencies": {
"@azure/identity": "^4.0.1",
"@azure/keyvault-secrets": "^4.7.0",
"@supabase/supabase-js": "^2.90.1",
"axios": "^1.13.2",
"bcrypt": "^5.1.1",
"connect-redis": "^7.1.0",
Expand Down
2 changes: 1 addition & 1 deletion backend/src/database/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
max: parseInt(process.env.DB_POOL_MAX || '10'),
min: parseInt(process.env.DB_POOL_MIN || '2'),
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000,
connectionTimeoutMillis: 30000,
allowExitOnIdle: false,
};

Expand All @@ -42,7 +42,7 @@
}
});

pool.on('connect', (_client: any) => {

Check warning on line 45 in backend/src/database/connection.ts

View workflow job for this annotation

GitHub Actions / CI - Lint and Build

Unexpected any. Specify a different type
logger.debug('Database connection established', {
totalCount: pool?.totalCount,
idleCount: pool?.idleCount,
Expand Down Expand Up @@ -89,9 +89,9 @@
}
};

export const queryWithRetry = async <T = any>(

Check warning on line 92 in backend/src/database/connection.ts

View workflow job for this annotation

GitHub Actions / CI - Lint and Build

Unexpected any. Specify a different type
queryText: string,
params?: any[],

Check warning on line 94 in backend/src/database/connection.ts

View workflow job for this annotation

GitHub Actions / CI - Lint and Build

Unexpected any. Specify a different type
maxRetries: number = 3
): Promise<T[]> => {
const pool = getDatabasePool();
Expand All @@ -101,7 +101,7 @@
try {
const result = await pool.query(queryText, params);
return result.rows;
} catch (error: any) {

Check warning on line 104 in backend/src/database/connection.ts

View workflow job for this annotation

GitHub Actions / CI - Lint and Build

Unexpected any. Specify a different type
lastError = error;

const isConnectionError =
Expand Down
18 changes: 18 additions & 0 deletions backend/src/database/migrations/010_create_posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('posts', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('user_id').references('id').inTable('users').onDelete('CASCADE').notNullable().index();
table.uuid('music_item_id').references('id').inTable('music_items').onDelete('CASCADE').notNullable().index();
table.integer('rating').notNullable().checkBetween([1, 10]);
table.text('text').nullable();
table.timestamps(true, true);
});

await knex.schema.raw('CREATE INDEX idx_posts_created_at ON posts(created_at DESC)');
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists('posts');
}
19 changes: 19 additions & 0 deletions backend/src/database/migrations/011_add_user_names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('users', (table) => {
table.string('first_name').nullable();
table.string('last_name').nullable();
table.uuid('supabase_auth_id').nullable();
});

await knex.schema.raw('CREATE INDEX idx_users_supabase_auth_id ON users(supabase_auth_id)');
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('users', (table) => {
table.dropColumn('first_name');
table.dropColumn('last_name');
table.dropColumn('supabase_auth_id');
});
}
12 changes: 9 additions & 3 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { rateLimitMiddleware } from './middleware/rate-limit.middleware';
import { logger } from './config/logger';


dotenv.config();

const app = express();
Expand Down Expand Up @@ -49,7 +50,7 @@
verify: (req: express.Request, _res: express.Response, buf: Buffer) => {
const url = req.url || req.originalUrl || req.path;
if (url.includes('webhooks') || url.includes('interactions')) {
(req as any).rawBody = buf;

Check warning on line 53 in backend/src/index.ts

View workflow job for this annotation

GitHub Actions / CI - Lint and Build

Unexpected any. Specify a different type
}
}
}));
Expand All @@ -61,8 +62,11 @@
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.get('/', (_req, res) => {
res.send('<h1>Welcome to MusIQ API</h1>');
});

import authRoutes from './routes/auth';
import oauthRoutes from './routes/oauth';
import profileRoutes from './routes/profile';
import musicRoutes from './routes/music';
import ratingRoutes from './routes/ratings';
Expand All @@ -71,9 +75,10 @@
import notificationRoutes from './routes/notifications';
import adminRoutes from './routes/admin';
import webhookRoutes from './routes/webhooks';
import postRoutes from './routes/posts';

app.use('/api/auth', authRoutes);
app.use('/api/auth/oauth', oauthRoutes);

app.use('/api/profile', profileRoutes);
app.use('/api/music', musicRoutes);
app.use('/api/ratings', ratingRoutes);
Expand All @@ -82,6 +87,7 @@
app.use('/api/notifications', notificationRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/webhooks', webhookRoutes);
app.use('/api/posts', postRoutes);

app.use('/interactions', webhookRoutes);

Expand All @@ -102,4 +108,4 @@
}
});

export default app;
export default app;
24 changes: 15 additions & 9 deletions backend/src/middleware/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { CustomError } from './error.middleware';
import { getDatabasePool } from '../database/connection';
import { supabaseService } from '../services/supabase.service';

export interface AuthRequest extends Request {
userId?: string;
Expand All @@ -22,19 +22,17 @@ export const authMiddleware = async (
}

const token = authHeader.substring(7);
const jwtSecret = process.env.JWT_SECRET || 'default-secret-change-in-production';
const pool = getDatabasePool();

const supabaseUser = await supabaseService.verifyAccessToken(token);

let decoded: any;
try {
decoded = jwt.verify(token, jwtSecret);
} catch (error) {
if (!supabaseUser) {
throw new CustomError('Invalid or expired token', 401);
}

const pool = getDatabasePool();
const userResult = await pool.query(
'SELECT id, email, role FROM users WHERE id = $1 AND deleted_at IS NULL',
[decoded.userId]
'SELECT id, email, role, email_verified FROM users WHERE supabase_auth_id = $1 AND deleted_at IS NULL',
[supabaseUser.userId]
);

if (userResult.rows.length === 0) {
Expand All @@ -43,6 +41,14 @@ export const authMiddleware = async (

const user = userResult.rows[0];

// Sync email_verified status if Supabase says it's verified but local DB says it's not
if (supabaseUser.emailVerified && !user.email_verified) {
await pool.query(
'UPDATE users SET email_verified = true, updated_at = NOW() WHERE id = $1',
[user.id]
);
}

req.userId = user.id;
req.userEmail = user.email;
req.userRole = user.role;
Expand Down
Loading
Loading