taskflow / backend /src /controllers /authController.js
Sughan-0077's picture
🚀 Complete Taskflow Suite: Initial Clean Release
f6c14df
Raw
History Blame Contribute Delete
6.21 kB
const bcrypt = require('bcryptjs');
const { v4: uuidv4 } = require('uuid');
const { query } = require('../utils/db');
const { generateToken, buildTokenPayload } = require('../utils/jwt');
const { auditLog } = require('../utils/auditLogger');
/**
* POST /api/auth/register
* Register a new organization + admin user
*/
const register = async (req, res) => {
try {
const { orgName, name, email, password } = req.body;
// Check email uniqueness across all orgs (global unique email)
const existing = await query(`SELECT id FROM users WHERE email = $1`, [email.toLowerCase()]);
if (existing.rows.length) {
return res.status(409).json({ error: 'An account with this email already exists.' });
}
// Generate org slug
const baseSlug = orgName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
let slug = baseSlug;
let suffix = 1;
while (true) {
const check = await query(`SELECT id FROM organizations WHERE slug = $1`, [slug]);
if (!check.rows.length) break;
slug = `${baseSlug}-${suffix++}`;
}
const orgId = uuidv4();
const userId = uuidv4();
const passwordHash = await bcrypt.hash(password, 12);
await query(`INSERT INTO organizations (id, name, slug) VALUES ($1, $2, $3)`, [orgId, orgName, slug]);
await query(
`INSERT INTO users (id, organization_id, name, email, password_hash, role) VALUES ($1, $2, $3, $4, $5, 'admin')`,
[userId, orgId, name, email.toLowerCase(), passwordHash]
);
const user = { id: userId, name, email: email.toLowerCase(), role: 'admin', organization_id: orgId, org_slug: slug };
const token = generateToken(buildTokenPayload(user));
await auditLog({
organizationId: orgId,
actorId: userId,
actorName: name,
actorEmail: email.toLowerCase(),
action: 'USER_REGISTERED',
entityType: 'user',
newValues: { userId, orgId, role: 'admin' },
});
return res.status(201).json({
token,
user: { id: userId, name, email: email.toLowerCase(), role: 'admin', organizationId: orgId, orgSlug: slug, orgName },
});
} catch (err) {
console.error('Register error:', err);
return res.status(500).json({ error: 'Registration failed.' });
}
};
/**
* POST /api/auth/login
*/
const login = async (req, res) => {
try {
const { email, password } = req.body;
const { rows } = await query(
`SELECT u.id, u.name, u.email, u.password_hash, u.role, u.organization_id, u.is_active,
o.slug as org_slug, o.name as org_name
FROM users u
JOIN organizations o ON o.id = u.organization_id
WHERE u.email = $1`,
[email.toLowerCase()]
);
if (!rows.length) {
return res.status(401).json({ error: 'Invalid email or password.' });
}
const user = rows[0];
if (!user.is_active) {
return res.status(403).json({ error: 'Your account has been deactivated.' });
}
if (!user.password_hash) {
return res.status(401).json({ error: 'This account uses OAuth login. Please sign in with your provider.' });
}
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
return res.status(401).json({ error: 'Invalid email or password.' });
}
const token = generateToken(buildTokenPayload(user));
return res.json({
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
organizationId: user.organization_id,
orgSlug: user.org_slug,
orgName: user.org_name,
},
});
} catch (err) {
console.error('Login error:', err);
return res.status(500).json({ error: 'Login failed.' });
}
};
/**
* GET /api/auth/me
*/
const getMe = async (req, res) => {
const u = req.user;
return res.json({
id: u.id,
name: u.name,
email: u.email,
role: u.role,
organizationId: u.organization_id,
orgSlug: u.org_slug,
orgName: u.org_name,
});
};
/**
* POST /api/auth/accept-invite
*/
const acceptInvite = async (req, res) => {
try {
const { token, name, password } = req.body;
const { rows: invites } = await query(
`SELECT * FROM invites WHERE token = $1 AND used_at IS NULL AND expires_at > NOW()`,
[token]
);
if (!invites.length) {
return res.status(400).json({ error: 'Invite link is invalid or has expired.' });
}
const invite = invites[0];
// Check email not already registered in this org
const dup = await query(`SELECT id FROM users WHERE email = $1`, [invite.email.toLowerCase()]);
if (dup.rows.length) {
return res.status(409).json({ error: 'An account with this email already exists.' });
}
const userId = uuidv4();
const passwordHash = await bcrypt.hash(password, 12);
await query(
`INSERT INTO users (id, organization_id, name, email, password_hash, role) VALUES ($1, $2, $3, $4, $5, $6)`,
[userId, invite.organization_id, name, invite.email.toLowerCase(), passwordHash, invite.role]
);
await query(`UPDATE invites SET used_at = NOW() WHERE id = $1`, [invite.id]);
const { rows: orgs } = await query(`SELECT slug, name FROM organizations WHERE id = $1`, [invite.organization_id]);
const org = orgs[0];
const user = { id: userId, name, email: invite.email, role: invite.role, organization_id: invite.organization_id, org_slug: org.slug };
const authToken = generateToken(buildTokenPayload(user));
await auditLog({
organizationId: invite.organization_id,
actorId: userId,
actorName: name,
actorEmail: invite.email.toLowerCase(),
action: 'USER_JOINED_VIA_INVITE',
entityType: 'user',
newValues: { userId, role: invite.role },
});
return res.status(201).json({
token: authToken,
user: { id: userId, name, email: invite.email, role: invite.role, organizationId: invite.organization_id, orgSlug: org.slug, orgName: org.name },
});
} catch (err) {
console.error('Accept invite error:', err);
return res.status(500).json({ error: 'Failed to accept invite.' });
}
};
module.exports = { register, login, getMe, acceptInvite };