File size: 7,824 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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
const { query } = require('../utils/db');
const { auditLog } = require('../utils/auditLogger');
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto');

/**
 * GET /api/admin/users
 * List all users in the admin's organization
 */
const listUsers = async (req, res) => {
  try {
    const { organization_id: orgId } = req.user;

    const { rows } = await query(
      `SELECT id, name, email, role, is_active, created_at, updated_at
       FROM users
       WHERE organization_id = $1
       ORDER BY created_at ASC`,
      [orgId]
    );

    return res.json(rows);
  } catch (err) {
    console.error('List users error:', err);
    return res.status(500).json({ error: 'Failed to retrieve users.' });
  }
};

/**
 * PATCH /api/admin/users/:id/role
 * Change a user's role
 */
const updateUserRole = async (req, res) => {
  try {
    const { organization_id: orgId, id: actorId, name: actorName, email: actorEmail } = req.user;
    const { id } = req.params;
    const { role } = req.body;

    if (!['admin', 'member'].includes(role)) {
      return res.status(400).json({ error: 'Role must be admin or member.' });
    }

    // Cannot change own role
    if (id === actorId) {
      return res.status(400).json({ error: 'You cannot change your own role.' });
    }

    const { rows } = await query(
      `UPDATE users SET role = $1, updated_at = NOW()
       WHERE id = $2 AND organization_id = $3
       RETURNING id, name, email, role, is_active`,
      [role, id, orgId]
    );

    if (!rows.length) {
      return res.status(404).json({ error: 'User not found in your organization.' });
    }

    await auditLog({
      organizationId: orgId,
      actorId,
      actorName,
      actorEmail,
      action: 'USER_ROLE_CHANGED',
      entityType: 'user',
      newValues: { userId: id, newRole: role },
    });

    return res.json(rows[0]);
  } catch (err) {
    console.error('Update role error:', err);
    return res.status(500).json({ error: 'Failed to update role.' });
  }
};

/**
 * PATCH /api/admin/users/:id/deactivate
 */
const deactivateUser = async (req, res) => {
  try {
    const { organization_id: orgId, id: actorId, name: actorName, email: actorEmail } = req.user;
    const { id } = req.params;

    if (id === actorId) {
      return res.status(400).json({ error: 'You cannot deactivate your own account.' });
    }

    const { rows } = await query(
      `UPDATE users SET is_active = false, updated_at = NOW()
       WHERE id = $1 AND organization_id = $2
       RETURNING id, name, email, role, is_active`,
      [id, orgId]
    );

    if (!rows.length) {
      return res.status(404).json({ error: 'User not found in your organization.' });
    }

    await auditLog({
      organizationId: orgId,
      actorId,
      actorName,
      actorEmail,
      action: 'USER_DEACTIVATED',
      entityType: 'user',
      newValues: { userId: id },
    });

    return res.json(rows[0]);
  } catch (err) {
    console.error('Deactivate user error:', err);
    return res.status(500).json({ error: 'Failed to deactivate user.' });
  }
};

/**
 * POST /api/admin/invites
 * Send an invite to join the organization
 */
const createInvite = async (req, res) => {
  try {
    const { organization_id: orgId, id: actorId, name: actorName, email: actorEmail } = req.user;
    const { email, role = 'member' } = req.body;

    if (!['admin', 'member'].includes(role)) {
      return res.status(400).json({ error: 'Role must be admin or member.' });
    }

    // Check if email already a member
    const existing = await query(`SELECT id FROM users WHERE email = $1`, [email.toLowerCase()]);
    if (existing.rows.length) {
      return res.status(409).json({ error: 'A user with this email already exists.' });
    }

    const token = crypto.randomBytes(32).toString('hex');
    const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days

    const { rows } = await query(
      `INSERT INTO invites (id, organization_id, email, role, token, invited_by, expires_at)
       VALUES ($1, $2, $3, $4, $5, $6, $7)
       RETURNING id, email, role, token, expires_at`,
      [uuidv4(), orgId, email.toLowerCase(), role, token, actorId, expiresAt]
    );

    await auditLog({
      organizationId: orgId,
      actorId,
      actorName,
      actorEmail,
      action: 'USER_INVITED',
      entityType: 'invite',
      newValues: { invitedEmail: email, role },
    });

    return res.status(201).json({
      ...rows[0],
      inviteUrl: `${process.env.FRONTEND_URL}/accept-invite?token=${token}`,
    });
  } catch (err) {
    console.error('Create invite error:', err);
    return res.status(500).json({ error: 'Failed to create invite.' });
  }
};

/**
 * GET /api/admin/invites
 */
const listInvites = async (req, res) => {
  try {
    const { organization_id: orgId } = req.user;

    const { rows } = await query(
      `SELECT i.id, i.email, i.role, i.expires_at, i.used_at, i.created_at,
              u.name AS invited_by_name
       FROM invites i
       LEFT JOIN users u ON u.id = i.invited_by
       WHERE i.organization_id = $1
       ORDER BY i.created_at DESC`,
      [orgId]
    );

    return res.json(rows);
  } catch (err) {
    console.error('List invites error:', err);
    return res.status(500).json({ error: 'Failed to retrieve invites.' });
  }
};

/**
 * GET /api/admin/audit-logs
 */
const getAuditLogs = async (req, res) => {
  try {
    const { organization_id: orgId } = req.user;
    const { page = 1, limit = 50, action, task_id } = req.query;

    const pageNum = Math.max(1, parseInt(page));
    const limitNum = Math.min(200, Math.max(1, parseInt(limit)));
    const offset = (pageNum - 1) * limitNum;

    const conditions = [`organization_id = $1`];
    const params = [orgId];
    let idx = 2;

    if (action) {
      conditions.push(`action = $${idx++}`);
      params.push(action);
    }
    if (task_id) {
      conditions.push(`task_id = $${idx++}`);
      params.push(task_id);
    }

    const where = `WHERE ${conditions.join(' AND ')}`;

    const countResult = await query(`SELECT COUNT(*) FROM audit_logs ${where}`, params);
    const total = parseInt(countResult.rows[0].count);

    const { rows } = await query(
      `SELECT id, task_id, actor_id, actor_name, actor_email, action, entity_type,
              old_values, new_values, metadata, created_at
       FROM audit_logs
       ${where}
       ORDER BY created_at DESC
       LIMIT $${idx} OFFSET $${idx + 1}`,
      [...params, limitNum, offset]
    );

    return res.json({
      logs: rows,
      pagination: { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) },
    });
  } catch (err) {
    console.error('Audit logs error:', err);
    return res.status(500).json({ error: 'Failed to retrieve audit logs.' });
  }
};

/**
 * GET /api/admin/org
 * Get organization info
 */
const getOrg = async (req, res) => {
  try {
    const { organization_id: orgId } = req.user;
    const { rows } = await query(`SELECT id, name, slug, created_at FROM organizations WHERE id = $1`, [orgId]);
    return res.json(rows[0]);
  } catch (err) {
    return res.status(500).json({ error: 'Failed to retrieve organization.' });
  }
};

/**
 * GET /api/users - org-scoped user list for dropdowns (any authenticated user)
 */
const listOrgUsers = async (req, res) => {
  try {
    const { organization_id: orgId } = req.user;
    const { rows } = await query(
      `SELECT id, name, email, role FROM users WHERE organization_id = $1 AND is_active = true ORDER BY name`,
      [orgId]
    );
    return res.json(rows);
  } catch (err) {
    return res.status(500).json({ error: 'Failed to retrieve users.' });
  }
};

module.exports = { listUsers, updateUserRole, deactivateUser, createInvite, listInvites, getAuditLogs, getOrg, listOrgUsers };