Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9685f40
Add interviewer question templates and answer keys
AndroidDevAccount Jul 28, 2026
170e3cc
Show answer keys in an automatic side panel
AndroidDevAccount Jul 28, 2026
5890613
Refresh answer-key panel styles
AndroidDevAccount Jul 28, 2026
11a3684
Document REST exercise with a public test API
AndroidDevAccount Jul 28, 2026
e185665
Make activity card draggable and refresh panel styles
AndroidDevAccount Jul 28, 2026
a71668f
Expand MVC validation interview exercise
AndroidDevAccount Jul 28, 2026
58fe846
Add sample data to SQL interview exercise
AndroidDevAccount Jul 28, 2026
7dc8335
Document LINQ employee exercise data
AndroidDevAccount Jul 28, 2026
2e05650
Explain SOLID in interviewer answer key
AndroidDevAccount Jul 28, 2026
9b2a022
Replace debugging prompt with easy running sum
AndroidDevAccount Jul 28, 2026
c8ceeba
Make interview overview candidate-facing
AndroidDevAccount Jul 28, 2026
a147d14
Add Entity Framework migration exercise
AndroidDevAccount Jul 28, 2026
5e4f5ed
Add AI prompting interview exercise
AndroidDevAccount Jul 28, 2026
0781582
Add responsive CSS interview exercise
AndroidDevAccount Jul 28, 2026
ec7628b
Protect interview templates behind admin auth
AndroidDevAccount Jul 28, 2026
4ce15a5
Restore admin login and recent session state
AndroidDevAccount Jul 28, 2026
99790e6
Use standard MVC validation in interview exercise
AndroidDevAccount Jul 28, 2026
52d4903
Align SOLID exercise with constructor injection
AndroidDevAccount Jul 28, 2026
e144f38
Start interview with insurance OOP model
AndroidDevAccount Jul 28, 2026
ee0e4dc
Load candidate interview overview first
AndroidDevAccount Jul 28, 2026
aab45c0
Make premium warm-up object-oriented
AndroidDevAccount Jul 28, 2026
5e53e45
Add reusable interview question sets
AndroidDevAccount Jul 28, 2026
6980ed4
Use doubles in premium warm-up
AndroidDevAccount Jul 28, 2026
3651c0b
Simplify policy card CSS exercise
AndroidDevAccount Jul 28, 2026
a5f85b2
Expand everyday CSS styling exercise
AndroidDevAccount Jul 28, 2026
f562a4e
Clarify mobile CSS media query
AndroidDevAccount Jul 28, 2026
7561365
Consolidate interview controls and add admin management
AndroidDevAccount Jul 28, 2026
96fc69f
Use one grouped question loader
AndroidDevAccount Jul 28, 2026
8651a11
Make dashboard reliably dismissible
AndroidDevAccount Jul 28, 2026
7de724c
Clarify interview question controls
AndroidDevAccount Jul 28, 2026
da166e3
Restore activity monitor and session dashboard pill
AndroidDevAccount Jul 28, 2026
51515bb
Position dashboard close button in corner
AndroidDevAccount Jul 28, 2026
3f8ab8b
Turn dashboard into resumable app page
AndroidDevAccount Jul 28, 2026
0c4be4c
Simplify activity window drag handle
AndroidDevAccount Jul 28, 2026
8e97b2a
Fix dashboard resume and session joining
AndroidDevAccount Jul 28, 2026
4df9517
Restore compact question and editor dropdowns
AndroidDevAccount Jul 28, 2026
7d29a1b
Clarify participant roles and add admin pill menu
AndroidDevAccount Jul 28, 2026
ff6f598
Persist server-backed interview question sets
AndroidDevAccount Jul 28, 2026
89ae36b
Consolidate connected user display
AndroidDevAccount Jul 28, 2026
0e2d682
Use standard activity drag icon
AndroidDevAccount Jul 28, 2026
f5cf73e
Load question sets at their introduction
AndroidDevAccount Jul 28, 2026
dcc2d11
Restore answer key for loaded questions
AndroidDevAccount Jul 28, 2026
07dc374
Hide session sharing from candidates
AndroidDevAccount Jul 28, 2026
354e99f
Address interview UI review feedback
AndroidDevAccount Jul 28, 2026
e4d06a2
Address follow-up PR review findings
AndroidDevAccount Jul 28, 2026
467ae02
Make admin creation atomic
AndroidDevAccount Jul 28, 2026
4f28131
Enable C sharp code execution
AndroidDevAccount Jul 29, 2026
c9c2261
Revert "Enable C sharp code execution"
AndroidDevAccount Jul 29, 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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ npm run dev
- **Timestamp Tracking:** Automatic timestamps for observations
- **Markdown Support:** Format notes with headings, lists, code blocks
- **Template System:** Use predefined interview question templates
- **Question Sets:** Choose a focused built-in set or [import custom questions and answer keys](docs/QUESTION_SETS.md)
- **Collaborative Notes:** Multiple interviewers can add notes simultaneously
- **Export Options:** Send to Slack, email, or download as PDF/JSON

Expand Down
113 changes: 113 additions & 0 deletions api/admins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use strict';

const admin = require('firebase-admin');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

if (!admin.apps.length && process.env.FIREBASE_PROJECT_ID) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n')
}),
databaseURL: process.env.FIREBASE_DATABASE_URL
});
}

function emailKey(email) {
return Buffer.from(email.trim().toLowerCase()).toString('base64url');
}

function requireAdmin(req) {
const header = req.headers.authorization || '';
if (!header.startsWith('Bearer ') || !process.env.JWT_SECRET) {
throw Object.assign(new Error('Authentication required'), { status: 401 });
}
const decoded = jwt.verify(header.slice(7), process.env.JWT_SECRET);
if (decoded.isAdmin !== true) {
throw Object.assign(new Error('Admin access required'), { status: 403 });
}
return decoded;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

module.exports = async function handler(req, res) {
res.setHeader('Cache-Control', 'private, no-store');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

if (req.method === 'OPTIONS') return res.status(200).end();
if (!['GET', 'POST'].includes(req.method)) {
return res.status(405).json({ error: 'Method not allowed' });
}

try {
const currentAdmin = requireAdmin(req);
if (!admin.apps.length) {
return res.status(503).json({ error: 'Admin storage is not configured' });
}
const adminsRef = admin.database().ref('adminAccounts');
const ownerEmail = String(process.env.ADMIN_EMAIL || '').trim().toLowerCase();

if (req.method === 'GET') {
const snapshot = await adminsRef.once('value');
const additionalAdmins = Object.values(snapshot.val() || {}).map(account => ({
email: account.email,
createdAt: account.createdAt,
createdBy: account.createdBy,
disabled: account.disabled === true,
owner: false
}));
const owner = ownerEmail
? [{ email: ownerEmail, owner: true }]
: [];
return res.status(200).json({ admins: [...owner, ...additionalAdmins] });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!ownerEmail || String(currentAdmin.email || '').trim().toLowerCase() !== ownerEmail) {
return res.status(403).json({ error: 'Only the configured owner can create admins' });
}

const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
const email = String(body?.email || '').trim().toLowerCase();
const password = String(body?.password || '');
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).json({ error: 'Enter a valid email address' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
if (email === ownerEmail) {
return res.status(409).json({ error: 'That admin already exists' });
}

const accountRef = adminsRef.child(emailKey(email));
const account = {
email,
passwordHash: await bcrypt.hash(password, 12),
createdAt: Date.now(),
createdBy: currentAdmin.email,
disabled: false
};
const result = await accountRef.transaction(currentValue => {
if (currentValue !== null) return;
return account;
});
if (!result.committed) {
return res.status(409).json({ error: 'That admin already exists' });
}

return res.status(201).json({ success: true, admin: { email, owner: false } });
} catch (error) {
if (error instanceof SyntaxError) {
return res.status(400).json({ error: 'Invalid JSON body' });
}
const authError = ['JsonWebTokenError', 'TokenExpiredError', 'NotBeforeError']
.includes(error.name);
const status = error.status || (authError ? 401 : 500);
if (status === 500) console.error('Admin management error:', error);
return res.status(status).json({
error: status === 500 ? 'Admin management failed' : error.message
});
}
};
49 changes: 36 additions & 13 deletions api/auth/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@

const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const admin = require('firebase-admin');

if (!admin.apps.length && process.env.FIREBASE_PROJECT_ID) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n')
}),
databaseURL: process.env.FIREBASE_DATABASE_URL
});
}

function emailKey(email) {
return Buffer.from(email.trim().toLowerCase()).toString('base64url');
}

// Admin credentials (use environment variables in production)
const ADMIN_EMAIL = process.env.ADMIN_EMAIL;
Expand Down Expand Up @@ -41,11 +57,6 @@ module.exports = async (req, res) => {
}
}

// Debug logging (remove in production)
console.log('Request body type:', typeof req.body);
console.log('Request body:', req.body);
console.log('Parsed body:', body);

const { email, password } = body || {};

if (!email || !password) {
Expand All @@ -61,21 +72,33 @@ module.exports = async (req, res) => {
}

try {
// Check email
if (email !== ADMIN_EMAIL) {
return res.status(401).json({ error: 'Invalid credentials' });
const normalizedEmail = String(email).trim().toLowerCase();
const normalizedOwnerEmail = ADMIN_EMAIL
? String(ADMIN_EMAIL).trim().toLowerCase()
: '';
const ownerLogin = Boolean(normalizedOwnerEmail) &&
normalizedEmail === normalizedOwnerEmail;
let passwordHash = ownerLogin ? ADMIN_PASSWORD_HASH : null;

if (!ownerLogin && admin.apps.length) {
const snapshot = await admin.database()
.ref(`adminAccounts/${emailKey(normalizedEmail)}`)
.once('value');
const account = snapshot.val();
if (account && account.disabled !== true) passwordHash = account.passwordHash;
}

// Verify password
const validPassword = await bcrypt.compare(password, ADMIN_PASSWORD_HASH);
const validPassword = passwordHash
? await bcrypt.compare(password, passwordHash)
: false;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}

// Generate token
const token = jwt.sign(
{
email: email,
email: normalizedEmail,
isAdmin: true,
userId: 'admin-' + Date.now()
},
Expand All @@ -86,10 +109,10 @@ module.exports = async (req, res) => {
res.status(200).json({
success: true,
token: token,
user: { email: email, isAdmin: true }
user: { email: normalizedEmail, isAdmin: true }
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Login failed' });
}
};
};
Loading