Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
4c9490a
Update package-lock.json
jktieh Nov 5, 2025
2e43270
Create Navbar.tsx
jktieh Nov 5, 2025
bd888ab
Create Hero.tsx
jktieh Nov 5, 2025
5db1770
Create Footer.tsx
jktieh Nov 5, 2025
78de9da
Rename to QRCode.tsx
jktieh Nov 5, 2025
f877a3f
Create Home.tsx
jktieh Nov 5, 2025
fe3c303
Create Card.tsx
jktieh Nov 5, 2025
a35e3e8
Update App.tsx
jktieh Nov 5, 2025
aa42265
Update index.html
jktieh Nov 5, 2025
4004201
Update Footer.tsx
jktieh Nov 5, 2025
48f1658
Update Hero.tsx
jktieh Nov 5, 2025
c228e72
Update Navbar.tsx
jktieh Nov 5, 2025
5a4e276
Update main.tsx
jktieh Nov 5, 2025
0408e96
Update Home.tsx
jktieh Nov 6, 2025
1509c7c
Update code structure for easy to maintain
lnqminh3003 Nov 7, 2025
cdf732c
Merge branch 'frontend/home-page' into frontend/qrcode
lnqminh3003 Nov 7, 2025
620232b
Updated homepage, added QR code component, about us section, redesign…
jktieh Nov 16, 2025
cf60e0f
Update Navbar.tsx
jktieh Nov 16, 2025
9fbcba0
Update Navbar.tsx
jktieh Nov 16, 2025
0afad34
Deleted unused components, updated Navbar
jktieh Dec 4, 2025
355108e
websocket hosting event
lnqminh3003 Jan 13, 2026
1dd3dc3
Merge branch 'frontend/qrcode' into frontend/home-page
lnqminh3003 Jan 19, 2026
91ac597
add vercel
lnqminh3003 Jan 19, 2026
9cb24a2
Merge branch 'backend' into frontend/home-page
lnqminh3003 Jan 20, 2026
6745299
Update the real time websocket for Frontend
lnqminh3003 Jan 20, 2026
bfce025
"STR-100 LogIn and SignUp logic for a host"
jktieh Jan 23, 2026
580b4d6
Added .env file to run our Frontend
youssefibrahim3 Jan 23, 2026
56e07a4
Fixed env
Jan 23, 2026
fc2928f
Added CreateEventPage
Jan 23, 2026
7b23b62
Added service/CreateEvent
Jan 23, 2026
eec9a9e
Create Event Page has now been fully re-integrated after it was lost …
Jan 23, 2026
184d2f0
Merge branch 'main' into frontend/home-page
lnqminh3003 Jan 26, 2026
e8c8bf5
Merge branch 'main' into frontend/home-page
lnqminh3003 Jan 26, 2026
8bcbf2a
Merge branch 'frontend/home-page' of github.com:techstartucalgary/Net…
lnqminh3003 Jan 26, 2026
9b68ec2
update gitignore file
lnqminh3003 Jan 26, 2026
40e9e09
add vercel.json file
lnqminh3003 Jan 26, 2026
0acb123
fix vercel.json file
lnqminh3003 Jan 26, 2026
30c537e
Add logo to login/sign up page
jktieh Jan 28, 2026
ded751c
Created the create-event page
Jan 29, 2026
9d1351a
Merge branch 'frontend/home-page' of https://github.com/techstartucal…
Jan 29, 2026
fba44f2
change joinCode example
lnqminh3003 Jan 30, 2026
be97e2c
Added logo button interactivity, and reworked create event page
Jan 30, 2026
91e85f5
Merge branch 'frontend/home-page' of https://github.com/techstartucal…
Jan 30, 2026
052bc98
added event page, responsive NavBar
jktieh Jan 30, 2026
c3f504c
Update Hero.tsx
jktieh Jan 30, 2026
1e4409d
Update Hero.tsx
jktieh Feb 2, 2026
0ecfefe
Adjust hero margin
jktieh Feb 2, 2026
ae5d5f4
update homepage
jktieh Feb 2, 2026
d16bea6
update create event
jktieh Feb 2, 2026
6f9a541
Merge branch 'main' into frontend/home-page
lnqminh3003 Feb 2, 2026
81ca8bb
Merge branch 'frontend/home-page' of github.com:techstartucalgary/Net…
lnqminh3003 Feb 2, 2026
1749f6e
fix data model in FE
lnqminh3003 Feb 2, 2026
a0a4d4c
update loggined in NavBar, added authUtils
jktieh Feb 6, 2026
a73a66c
Created BingoGame.ts to call the Bingo creation api
Feb 6, 2026
39ada11
update DashboardPage.tsx
jktieh Feb 6, 2026
e3ec900
update navbar
jktieh Feb 6, 2026
cdc833f
update about us bug
jktieh Feb 6, 2026
5b796d0
Fetch created events by user id
lnqminh3003 Feb 7, 2026
98cf0b2
Merge branch 'main' into backend
lnqminh3003 Feb 7, 2026
313c97e
Merge pull request #56 from techstartucalgary/fetch_created_event_by_…
lnqminh3003 Feb 7, 2026
f80f0b3
update dashboard
jktieh Feb 7, 2026
ea1ac89
Update HomePage.tsx
jktieh Feb 7, 2026
16ebb99
Refactor get bingo game by eventId
lnqminh3003 Feb 8, 2026
08ca74b
Merge pull request #57 from techstartucalgary/fix-get-bingo-game-endp…
lnqminh3003 Feb 8, 2026
25fd441
added bingo game
jktieh Feb 8, 2026
8d78da7
Add LinkedIn OAuth 2.0 authentication
rxmox Feb 8, 2026
10b3e35
Update EventPage.tsx
jktieh Feb 9, 2026
a4cab4b
update dashboard layout, fix bug
jktieh Feb 9, 2026
c808fa7
Merge pull request #58 from techstartucalgary/linkedin-oauth
lnqminh3003 Feb 9, 2026
3447435
Update DashboardPage.tsx
jktieh Feb 9, 2026
4b17945
Fix broken logo
lnqminh3003 Feb 9, 2026
a8cbd4f
Merge branch 'frontend/home-page' of github.com:techstartucalgary/Net…
lnqminh3003 Feb 9, 2026
1772b29
Add secure auth code exchange for LinkedIn OAuth callback
rxmox Feb 16, 2026
4521286
Add quick signup via guest join and profile update endpoint
rxmox Feb 16, 2026
a9c4161
Merge pull request #59 from techstartucalgary/code-auth
lnqminh3003 Feb 17, 2026
987cc82
Merge pull request #60 from techstartucalgary/quick-signup
lnqminh3003 Feb 17, 2026
dff5657
Merge pull request #51 from techstartucalgary/frontend/home-page
lnqminh3003 Feb 17, 2026
2efc545
Merge pull request #55 from techstartucalgary/mobile-and-backend
lnqminh3003 Feb 17, 2026
cb0cda3
Merge pull request #61 from techstartucalgary/backend
lnqminh3003 Feb 17, 2026
2c3a7d9
(Update) Welcome and Event Lobby Page Created
keeryn04 Feb 18, 2026
d9f3a03
(Fix) Bingo Categories Loading Correctly
keeryn04 Feb 18, 2026
dc996fe
(Fix) Name Bingo Logout Fix
keeryn04 Feb 18, 2026
68c09e7
(Update) Added Bingo Winning State Checks
keeryn04 Feb 19, 2026
c03076b
Merge pull request #62 from techstartucalgary/mobile-extra-pages
tahaminachy43 Feb 19, 2026
a35ed7e
Get Started styling
tahaminachy43 Feb 20, 2026
7e7b6c1
Merge pull request #63 from techstartucalgary/mobile-signin-styling
lnqminh3003 Feb 20, 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
Binary file added .DS_Store
Binary file not shown.
38 changes: 38 additions & 0 deletions shatter-backend/package-lock.json

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

1 change: 1 addition & 0 deletions shatter-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.13.5",
"bcryptjs": "^3.0.3",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
Expand Down
Binary file added shatter-backend/src/.DS_Store
Binary file not shown.
166 changes: 165 additions & 1 deletion shatter-backend/src/controllers/auth_controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { Request, Response } from 'express';
import { User } from '../models/user_model';
import { AuthCode } from '../models/auth_code_model';
import { hashPassword, comparePassword } from '../utils/password_hash';
import { generateToken } from '../utils/jwt_utils';
import { getLinkedInAuthUrl, getLinkedInAccessToken, getLinkedInProfile } from '../utils/linkedin_oauth';

const JWT_SECRET = process.env.JWT_SECRET || '';

// Email validation regex
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
Expand Down Expand Up @@ -140,7 +146,12 @@ export const login = async (req: Request, res: Response) => {
});
}

// 7 - verify password
// 7 - verify password (OAuth users won't have a passwordHash)
if (!user.passwordHash) {
return res.status(401).json({
error: 'This account uses LinkedIn login. Please sign in with LinkedIn.',
});
}
const isPasswordValid = await comparePassword(password, user.passwordHash);

if (!isPasswordValid) {
Expand Down Expand Up @@ -173,3 +184,156 @@ export const login = async (req: Request, res: Response) => {
}
};


/**
* GET /api/auth/linkedin
* Initiates LinkedIn OAuth flow by redirecting to LinkedIn
*/
export const linkedinAuth = async (req: Request, res: Response) => {
try {
// Generate CSRF protection state token
const state = crypto.randomBytes(16).toString('hex');

// Encode state as JWT with 5-minute expiration (stateless validation)
const stateToken = jwt.sign({ state }, JWT_SECRET, { expiresIn: '5m' });

// Build LinkedIn authorization URL and redirect
const authUrl = getLinkedInAuthUrl(stateToken);
res.redirect(authUrl);
} catch (error) {
console.error('LinkedIn auth initiation error:', error);
res.status(500).json({ error: 'Failed to initiate LinkedIn authentication' });
}
};


/**
* GET /api/auth/linkedin/callback
* LinkedIn redirects here after user authorization
*/
export const linkedinCallback = async (req: Request, res: Response) => {
try {
const { code, state, error: oauthError } = req.query as {
code?: string;
state?: string;
error?: string;
};

const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:19006';

// Handle user denial
if (oauthError === 'user_cancelled_authorize') {
return res.redirect(`${frontendUrl}/auth/error?message=Authorization cancelled`);
}

// Validate required parameters
if (!code || !state) {
return res.status(400).json({ error: 'Missing code or state parameter' });
}

// Verify state token (CSRF protection)
try {
jwt.verify(state, JWT_SECRET);
} catch {
return res.status(401).json({ error: 'Invalid state parameter' });
}

// Exchange code for access token
const accessToken = await getLinkedInAccessToken(code);

// Fetch user profile from LinkedIn
const linkedinProfile = await getLinkedInProfile(accessToken);

// Validate email is provided
if (!linkedinProfile.email) {
return res.status(400).json({
error: 'Email address required',
suggestion: 'Please make your email visible to third-party apps in LinkedIn settings',
});
}

// Find existing user by LinkedIn ID
let user = await User.findOne({ linkedinId: linkedinProfile.sub });

if (!user) {
// Check if email already exists with password auth (email conflict)
const existingEmailUser = await User.findOne({
email: linkedinProfile.email.toLowerCase().trim(),
});

if (existingEmailUser) {
return res.redirect(
`${frontendUrl}/auth/error?message=Email already registered with password&suggestion=Please login with your password`
);
}

// Create new user from LinkedIn data
user = await User.create({
name: linkedinProfile.name,
email: linkedinProfile.email.toLowerCase().trim(),
linkedinId: linkedinProfile.sub,
profilePhoto: linkedinProfile.picture,
authProvider: 'linkedin',
lastLogin: new Date(),
});
} else {
// Update existing user's last login
user.lastLogin = new Date();
await user.save();
}

// Generate single-use auth code and redirect to frontend
const authCode = crypto.randomBytes(32).toString('hex');
await AuthCode.create({ code: authCode, userId: user._id });
return res.redirect(`${frontendUrl}/auth/callback?code=${authCode}`);

} catch (error: any) {
console.error('LinkedIn callback error:', error);

const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:19006';
if (error.message?.includes('LinkedIn')) {
return res.redirect(`${frontendUrl}/auth/error?message=LinkedIn authentication failed`);
}

res.status(500).json({ error: 'Authentication failed. Please try again.' });
}
};


/**
* POST /api/auth/exchange
* Exchange a single-use auth code for a JWT token
*
* @param req.body.code - The auth code from the OAuth callback redirect
* @returns 200 with userId and JWT token on success
*/
export const exchangeAuthCode = async (req: Request, res: Response) => {
try {
const { code } = req.body as { code?: string };

if (!code) {
return res.status(400).json({ error: 'Auth code is required' });
}

// Find and delete the auth code in one atomic operation (single-use)
const authCodeDoc = await AuthCode.findOneAndDelete({ code });

if (!authCodeDoc) {
return res.status(401).json({ error: 'Invalid or expired auth code' });
}

// Generate JWT token
const token = generateToken(authCodeDoc.userId.toString());

res.status(200).json({
message: 'Authentication successful',
userId: authCodeDoc.userId,
token,
});

} catch (err: any) {
console.error('POST /api/auth/exchange error:', err);
res.status(500).json({ error: 'Failed to exchange auth code' });
}
};

16 changes: 6 additions & 10 deletions shatter-backend/src/controllers/bingo_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,25 +72,21 @@ export async function createBingo(req: Request, res: Response) {
}
}


/**
* @param req.body.id - Bingo _id (string) OR Event _id (ObjectId string) (required)
*/
export async function getBingo(req: Request, res: Response) {
try {
const { id } = req.body;
const { eventId } = req.params;

if (!id) {
if (!eventId) {
return res.status(400).json({
success: false,
msg: "id is required",
msg: "eventId is required",
});
}

let bingo = await Bingo.findById(id);
let bingo = await Bingo.findById(eventId);

if (!bingo && Types.ObjectId.isValid(id)) {
bingo = await Bingo.findOne({ _eventId: id });
if (!bingo && Types.ObjectId.isValid(eventId)) {
bingo = await Bingo.findOne({ _eventId: eventId });
}

if (!bingo) {
Expand Down
60 changes: 57 additions & 3 deletions shatter-backend/src/controllers/event_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { pusher } from "../utils/pusher_websocket";
import "../models/participant_model";

import { generateJoinCode } from "../utils/event_utils";
import { generateToken } from "../utils/jwt_utils";
import { Participant } from "../models/participant_model";
import { User } from "../models/user_model";
import { Types } from "mongoose";
Expand Down Expand Up @@ -247,20 +248,33 @@ export async function joinEventAsGuest(req: Request, res: Response) {
return res.status(400).json({ success: false, msg: "Event is full" });
}

// Create guest participant (userId is null)
// Create a guest user account so they get a JWT and can complete their profile later
const user = await User.create({
name,
authProvider: 'guest',
});

const userId = user._id as Types.ObjectId;
const token = generateToken(userId.toString());

// Create participant linked to the new user
const participant = await Participant.create({
userId: null,
userId,
name,
eventId,
});

const participantId = participant._id as Types.ObjectId;

// Add participant to event
// Add participant to event and event to user history
await Event.updateOne(
{ _id: eventId },
{ $addToSet: { participantIds: participantId } },
);
await User.updateOne(
{ _id: userId },
{ $addToSet: { eventHistoryIds: eventId } },
);

// Emit socket
console.log("Room socket:", eventId);
Expand All @@ -278,6 +292,8 @@ export async function joinEventAsGuest(req: Request, res: Response) {
return res.json({
success: true,
participant,
userId,
token,
});
} catch (e: any) {
if (e.code === 11000) {
Expand Down Expand Up @@ -328,3 +344,41 @@ export async function getEventById(req: Request, res: Response) {
res.status(500).json({ success: false, error: err.message });
}
}

/**
* GET /api/events/createdEvents/user/:userId
* Get list of events created by a specific user
*
* @param req.params.userId - User ID (required)
*
* @returns 200 with list of events on success
* @returns 400 if userId is missing
* @returns 404 if no events are found for the user
*/
export async function getEventsByUserId(req: Request, res: Response) {
try {
const { userId } = req.params;

if (!userId) {
return res
.status(400)
.json({ success: false, error: "userId is required" });
}

const events = await Event.find({ createdBy: userId });

if (!events || events.length === 0) {
return res.status(404).json({
success: false,
error: "No events found for this user",
});
}

res.status(200).json({
success: true,
events,
});
} catch (err: any) {
res.status(500).json({ success: false, error: err.message });
}
}
Loading