Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bbd5590
feat: client-side media swap for resource view (no full reload on rel…
OlivierJM Apr 19, 2026
1b51ccd
fix: restore dbClient null return on connection failure, add retry on…
OlivierJM Apr 19, 2026
3a37291
fix media navigation
OlivierJM Apr 19, 2026
d02d130
feat: add versioned MongoDB migration system
OlivierJM Apr 19, 2026
db85f5f
fix: add Migration type + error logging to migration runner
OlivierJM Apr 19, 2026
b621dc4
feat: add migrations 002-005, CLI script, and npm migrate script
OlivierJM Apr 19, 2026
860a8bc
feat: add migrations 002-005, CLI script, and npm migrate script
OlivierJM Apr 19, 2026
a8acd6a
fix: use tsx for migrate script, add try/finally for connection cleanup
OlivierJM Apr 19, 2026
59353ae
fix: preserve media items with no linked user in p_fetchRandomMediaCo…
OlivierJM Apr 19, 2026
58e1a47
fix: replace zfd.formData with z.object in user edit handler (#427)
OlivierJM Apr 19, 2026
75c8ccd
fix: schema bugs in schema.ts, collections.ts, and types
OlivierJM Apr 19, 2026
c3063f6
fix: update user_rolesSchema permission_ids to objectId[]
OlivierJM Apr 19, 2026
b8cb4d7
fix: add external_url to media types, use safeParse in user edit for …
OlivierJM Apr 19, 2026
af860de
fix: safety comments for unique indexes, idempotency note, INVALID_IN…
OlivierJM Apr 19, 2026
4a0881a
fix: suppress hydration warning for theme, LCP image priority, 5s DB …
OlivierJM Apr 19, 2026
dfe6432
remove unused files
OlivierJM Apr 19, 2026
5b10acd
clean up and fix user mappings
OlivierJM Apr 19, 2026
ffd2936
merge origin/main: remove flowbite, drop migrations, resolve conflicts
OlivierJM Apr 25, 2026
f4c0e45
Minor fixes
OlivierJM Apr 25, 2026
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"postcss": "^8.4.28",
"tailwindcss": "^3.3.3",
"ts-node": "^10.9.2",
"tsx": "^4.21.0",
"vitest": "^4.1.0"
},
"resolutions": {
Expand Down
21 changes: 17 additions & 4 deletions src/app/api/auth/authOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@
CredentialsProvider({
name: 'Credentials',
credentials: {},
// @ts-expect-error

Check failure on line 12 in src/app/api/auth/authOptions.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unused '@ts-expect-error' directive.
async authorize(credentials) {
// @ts-expect-error
const { jwtToken } = credentials;
const { jwtToken, id, email, role, firstName, lastName, phoneNumber, avatar } = credentials;

try {
const user = { ...credentials }; // Extract user details from credentials
return { ...user, token: jwtToken };
return { id, token: jwtToken, email, role, firstName, lastName, phone: phoneNumber, avatar };
} catch {
return null;
}
Expand All @@ -28,23 +27,37 @@
},
callbacks: {
async session({ session, token }) {
if (token.sub && session.user) {
if (session.user) {
session.user.id = token.sub;
session.user.role = token.role as string;
session.role = token.role as string;
session.user.firstName = token.firstName as string | undefined;
session.user.lastName = token.lastName as string | undefined;
session.user.phone = token.phone as string | undefined;
session.user.avatar = token.avatar as string | undefined;
}
return session;
},
async jwt({ token, user }) {

Check failure on line 41 in src/app/api/auth/authOptions.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Type '({ token, user }: { token: JWT; user: User | AdapterUser; account: Account | null; profile?: Profile | undefined; trigger?: "signIn" | "signUp" | "update" | undefined; isNewUser?: boolean | undefined; session?: any; }) => Promise<...>' is not assignable to type '(params: { token: JWT; user: User | AdapterUser; account: Account | null; profile?: Profile | undefined; trigger?: "signIn" | "signUp" | "update" | undefined; isNewUser?: boolean | undefined; session?: any; }) => Awaitable<...>'.
if (user) {
token.id = user.id;
token.email = user.email;
token.role = user.role ?? token.role;
token.firstName = user.firstName;
token.lastName = user.lastName;
token.phone = user.phone;
token.avatar = user.avatar;
}

if (token.sub) {
const db = await dbClient();
if (db) {
const dbUser = await db
.collection(dbCollections.users.name)
.findOne({ _id: new BSON.ObjectId(token.sub) }, { projection: { _id: 1 } });

if (!dbUser) return null; // invalidate session if user no longer exists

const roleMapping = await db.collection(dbCollections.user_role_mappings.name).findOne({
user_id: new BSON.ObjectId(token.sub),
});
Expand Down
32 changes: 28 additions & 4 deletions src/app/api/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export default async function login_(request: Request) {
role: 1,
is_verified: 1,
password: 1,
firstName: 1,
lastName: 1,
phoneNumber: 1,
avatar: 1,
},
},
);
Expand All @@ -67,17 +71,33 @@ export default async function login_(request: Request) {
});
}

const userRole = await db
let userRoleResult = await db
.collection(dbCollections.user_role_mappings.name)
.aggregate(p_fetchUserRoleDetails({ userId: `${user._id}` }))
.toArray();

if (!userRoleResult[0]) {
const defaultRole = await db
.collection(dbCollections.user_roles.name)
.findOne({ name: 'student' });

if (defaultRole) {
await db.collection(dbCollections.user_role_mappings.name).insertOne({
user_id: user._id,
role_id: defaultRole._id,
created_at: new Date(),
});

userRoleResult = [{ role_details: defaultRole }];
}
}

let role: null | T_RECORD = null;

if (userRole[0]) {
if (userRoleResult[0]) {
role = {
id: userRole[0].role_details._id,
name: userRole[0].role_details.name,
id: userRoleResult[0].role_details._id,
name: userRoleResult[0].role_details.name,
};
}

Expand All @@ -92,6 +112,10 @@ export default async function login_(request: Request) {
id: user._id.toString(),
email: user.email,
role: role?.name,
firstName: user.firstName,
lastName: user.lastName,
phoneNumber: user.phoneNumber,
avatar: user.avatar,
},
jwtToken: token,
};
Expand Down
15 changes: 14 additions & 1 deletion src/app/api/auth/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,20 @@ export default async function signup_(request: Request) {
userDoc.grade = grade;
}

await db.collection(dbCollections.users.name).insertOne(userDoc);
const studentRole = await db.collection(dbCollections.user_roles.name).findOne({ name: 'student' });
if (!studentRole) {
return new Response(JSON.stringify({ isError: true, code: SPARKED_PROCESS_CODES.DB_CONNECTION_FAILED }), {
status: HttpStatusCode.InternalServerError,
});
}

const insertResult = await db.collection(dbCollections.users.name).insertOne(userDoc);

await db.collection(dbCollections.user_role_mappings.name).insertOne({
user_id: insertResult.insertedId,
role_id: studentRole._id,
created_at: new Date(),
});

await resend.emails.send({
from: 'Sparked Support <support@sparkednext.app>',
Expand Down
10 changes: 9 additions & 1 deletion src/app/api/lib/db/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const dbCollections: T_dbCollection = {
},
page_links: {
name: 'page-links',
label: 'Media Content',
label: 'Page Links',
},
grades: {
name: 'grades',
Expand Down Expand Up @@ -73,4 +73,12 @@ export const dbCollections: T_dbCollection = {
name: 'media_reactions',
label: 'Media Reactions',
},
institution_memberships: {
name: 'institution_memberships',
label: 'Institution Memberships',
},
institution_invites: {
name: 'institution_invites',
label: 'Institution Invites',
},
} as const;
34 changes: 23 additions & 11 deletions src/app/api/lib/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
import { MongoClient } from "mongodb";
import { MongoClient } from 'mongodb';

if (!process.env.MONGODB_URI) {
console.error('Invalid/Missing environment variable: "MONGODB_URI"');
}

const uri = process.env.MONGODB_URI || "mongodb://...."; // Just to allow the build to pass
const options = {};
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/sparked'; // Just to allow the build to pass
const options = {
serverSelectionTimeoutMS: 5000,
connectTimeoutMS: 5000,
};

declare global {
// eslint-disable-next-line no-var
var _mongoClientPromise: Promise<MongoClient> | undefined;
}

if (!global._mongoClientPromise) {
const client = new MongoClient(uri, options);
global._mongoClientPromise = client.connect();
function getClientPromise(): Promise<MongoClient> {
if (!global._mongoClientPromise) {
const client = new MongoClient(uri, options);
global._mongoClientPromise = client.connect().catch((err) => {
// Reset so the next request gets a fresh attempt
global._mongoClientPromise = undefined;
return Promise.reject(err);
});
}
return global._mongoClientPromise;
}

const mongoClientPromise = global._mongoClientPromise;

export const dbClient = async () => {
const client = await mongoClientPromise;
return client.db(process.env.MONGODB_DB);
try {
const client = await getClientPromise();
return client.db(process.env.MONGODB_DB);
} catch {
return null;
}
};

export default mongoClientPromise;
export default getClientPromise();
7 changes: 5 additions & 2 deletions src/app/api/lib/db/init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { addDefaultRoles } from './migrations/addDefaultRoles';
import { dbClient } from '.';
import { runMigrations } from './migrate';

Check failure on line 2 in src/app/api/lib/db/init.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Cannot find module './migrate' or its corresponding type declarations.

export async function initializeDatabase() {
await addDefaultRoles();
const db = await dbClient();
if (!db) return;
await runMigrations(db);
}
37 changes: 0 additions & 37 deletions src/app/api/lib/db/migrations/addDefaultRoles.ts

This file was deleted.

64 changes: 55 additions & 9 deletions src/app/api/lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const coursesSchema = {
code: 'string',
created_at: 'date',
created_by_id: 'objectId',
institutions_id: 'objectId?',
institution_id: 'objectId?',
is_visible: 'bool?',
languages: 'courses_languages []',
name: 'string',
Expand All @@ -97,7 +97,7 @@ export const courses_languagesSchema = {
};

export type feed_back = {
_id?: ObjectId;
_id: ObjectId;
attachment_link?: string;
created_at?: Date;
message?: string;
Expand All @@ -108,7 +108,7 @@ export type feed_back = {
export const feed_backSchema = {
name: 'feed_back',
properties: {
_id: 'objectId?',
_id: 'objectId',
attachment_link: 'string?',
created_at: 'date?',
message: 'string?',
Expand Down Expand Up @@ -185,6 +185,52 @@ export const institutionsSchema = {
primaryKey: '_id',
};

export type institution_memberships = {
_id: ObjectId;
user_id: ObjectId;
institution_id: ObjectId;
role: 'admin' | 'member';
joined_at: Date;
invited_by_id?: ObjectId;
};

export const institution_membershipsSchema = {
name: 'institution_memberships',
properties: {
_id: 'objectId',
user_id: 'objectId',
institution_id: 'objectId',
role: 'string',
joined_at: 'date',
invited_by_id: 'objectId?',
},
primaryKey: '_id',
};

export type institution_invites = {
_id: ObjectId;
institution_id: ObjectId;
email: string;
token: string;
invited_by_id: ObjectId;
expires_at: Date;
used_at?: Date;
};

export const institution_invitesSchema = {
name: 'institution_invites',
properties: {
_id: 'objectId',
institution_id: 'objectId',
email: 'string',
token: 'string',
invited_by_id: 'objectId',
expires_at: 'date',
used_at: 'date?',
},
primaryKey: '_id',
};

// Keep schools schema for backward compatibility (will be migrated to institutions)
export const schoolsSchema = {
name: 'schools',
Expand All @@ -207,7 +253,7 @@ export type preferences = {
description?: string;
title?: string;
updated_at?: Date;
updated_by_id?: Date;
updated_by_id?: ObjectId;
};

export const preferencesSchema = {
Expand All @@ -219,7 +265,7 @@ export const preferencesSchema = {
description: 'string?',
title: 'string?',
updated_at: 'date?',
updated_by_id: 'date?',
updated_by_id: 'objectId?',
},
primaryKey: '_id',
};
Expand All @@ -230,7 +276,7 @@ export type settings = {
created_by_id: ObjectId;
description?: string;
title: string;
upated_at?: Date;
updated_at?: Date;
updated_by_id?: ObjectId;
};

Expand All @@ -242,7 +288,7 @@ export const settingsSchema = {
created_by_id: 'objectId',
description: 'string?',
title: 'string',
upated_at: 'date?',
updated_at: 'date?',
updated_by_id: 'objectId?',
},
primaryKey: '_id',
Expand Down Expand Up @@ -281,7 +327,7 @@ export type user_roles = {
description?: string;
label?: string;
name?: string;
permission_ids: user_permissions[];
permission_ids: ObjectId[];
updated_at?: Date;
updated_by_id?: ObjectId;
};
Expand All @@ -295,7 +341,7 @@ export const user_rolesSchema = {
description: 'string?',
label: 'string?',
name: 'string?',
permission_ids: 'user_permissions[]',
permission_ids: 'objectId[]',
updated_at: 'date?',
updated_by_id: 'objectId?',
},
Expand Down
Loading
Loading