File size: 6,208 Bytes
f6c14df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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 };