241 lines
8.1 KiB
TypeScript
241 lines
8.1 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import { query } from '../config/database.js';
|
|
import { snakeToCamel } from '../utils/mapper.js';
|
|
|
|
interface IAppUser_Postgres {
|
|
id: string;
|
|
logto_id: string;
|
|
email: string;
|
|
name: string;
|
|
role: 'guest' | 'user' | 'admin';
|
|
is_banned: boolean;
|
|
organization_id?: string;
|
|
organization_role?: string;
|
|
organization_banned?: boolean;
|
|
}
|
|
|
|
interface AuthRequest extends Request {
|
|
appUser?: IAppUser_Postgres | any;
|
|
}
|
|
|
|
export const syncUser = async (req: Request, res: Response) => {
|
|
try {
|
|
const { logtoId, email, name, organizationId, incomingRole } = req.body;
|
|
|
|
if (!logtoId || !email || !name) {
|
|
return res.status(400).json({ error: 'logtoId, email e name são obrigatórios.' });
|
|
}
|
|
|
|
const userResult = await query(
|
|
`INSERT INTO users (logto_id, email, name, role, is_banned, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, NOW())
|
|
ON CONFLICT (logto_id)
|
|
DO UPDATE SET email = EXCLUDED.email, name = EXCLUDED.name, updated_at = NOW()
|
|
RETURNING id, logto_id, email, name, role, is_banned`,
|
|
[logtoId, email, name, 'guest', false]
|
|
);
|
|
|
|
const user = userResult?.rows[0];
|
|
|
|
if (organizationId) {
|
|
let appRole = 'guest';
|
|
if (incomingRole === 'org:admin') appRole = 'admin';
|
|
else if (incomingRole === 'org:member') appRole = 'user';
|
|
|
|
const memberResult = await query(
|
|
`INSERT INTO user_organizations (user_id, organization_id, role, is_banned, updated_at)
|
|
VALUES ($1, $2, $3, $4, NOW())
|
|
ON CONFLICT (user_id, organization_id)
|
|
DO UPDATE SET updated_at = NOW()
|
|
RETURNING role, is_banned`,
|
|
[user.id, organizationId, appRole, false]
|
|
);
|
|
|
|
const member = memberResult?.rows[0];
|
|
|
|
return res.json(snakeToCamel({
|
|
...user,
|
|
organization_role: member.role,
|
|
organization_banned: member.is_banned
|
|
}));
|
|
}
|
|
|
|
res.json(snakeToCamel(user));
|
|
} catch (error) {
|
|
console.error('Error syncing user:', error);
|
|
res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + (error instanceof Error ? error.message : String(error)) });
|
|
}
|
|
};
|
|
|
|
export const getCurrentUser = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
if (!req.appUser) {
|
|
return res.status(404).json({ error: 'Usuário não encontrado.' });
|
|
}
|
|
|
|
const organizationId = req.headers['x-organization-id'] as string;
|
|
|
|
if (organizationId) {
|
|
const memberResult = await query(
|
|
'SELECT role, is_banned FROM user_organizations WHERE user_id = $1 AND organization_id = $2',
|
|
[req.appUser.id, organizationId]
|
|
);
|
|
|
|
if (memberResult?.rows[0]) {
|
|
const member = memberResult.rows[0];
|
|
return res.json(snakeToCamel({
|
|
...req.appUser,
|
|
role: member.role,
|
|
is_banned: member.is_banned,
|
|
organization_id: organizationId
|
|
}));
|
|
}
|
|
}
|
|
|
|
res.json(snakeToCamel(req.appUser));
|
|
} catch (error) {
|
|
console.error('Error getting current user:', error);
|
|
res.status(500).json({ error: 'Erro ao buscar usuário.' });
|
|
}
|
|
};
|
|
|
|
export const getAllUsers = async (req: Request, res: Response) => {
|
|
try {
|
|
const organizationId = req.headers['x-organization-id'] as string;
|
|
|
|
if (!organizationId) {
|
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
|
}
|
|
|
|
const result = await query(
|
|
`SELECT u.id, u.email, u.name, uo.role, uo.is_banned, uo.created_at
|
|
FROM users u
|
|
JOIN user_organizations uo ON u.id = uo.user_id
|
|
WHERE uo.organization_id = $1
|
|
ORDER BY uo.created_at DESC`,
|
|
[organizationId]
|
|
);
|
|
|
|
res.json((result?.rows || []).map(snakeToCamel));
|
|
} catch (error) {
|
|
console.error('Error getting users:', error);
|
|
res.status(500).json({ error: 'Erro ao buscar usuários.' });
|
|
}
|
|
};
|
|
|
|
export const updateUserRole = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { role } = req.body;
|
|
const organizationId = req.headers['x-organization-id'] as string;
|
|
|
|
if (!organizationId) {
|
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
|
}
|
|
|
|
if (!['guest', 'user', 'admin'].includes(role)) {
|
|
return res.status(400).json({ error: 'Role inválido.' });
|
|
}
|
|
|
|
const result = await query(
|
|
'UPDATE user_organizations SET role = $1, updated_at = NOW() WHERE user_id = $2 AND organization_id = $3 RETURNING *',
|
|
[role, id, organizationId]
|
|
);
|
|
|
|
if (!result?.rowCount) {
|
|
return res.status(404).json({ error: 'Membro não encontrado nesta organização.' });
|
|
}
|
|
|
|
res.json(snakeToCamel(result.rows[0]));
|
|
} catch (error) {
|
|
console.error('Error updating role:', error);
|
|
res.status(500).json({ error: 'Erro ao alterar role.' });
|
|
}
|
|
};
|
|
|
|
export const toggleBanUser = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { isBanned } = req.body;
|
|
const organizationId = req.headers['x-organization-id'] as string;
|
|
|
|
if (!organizationId) {
|
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
|
}
|
|
|
|
const result = await query(
|
|
'UPDATE user_organizations SET is_banned = $1, updated_at = NOW() WHERE user_id = $2 AND organization_id = $3 RETURNING *',
|
|
[isBanned, id, organizationId]
|
|
);
|
|
|
|
if (!result?.rowCount) {
|
|
return res.status(404).json({ error: 'Membro não encontrado.' });
|
|
}
|
|
|
|
res.json(snakeToCamel(result.rows[0]));
|
|
} catch (error) {
|
|
console.error('Error toggling ban:', error);
|
|
res.status(500).json({ error: 'Erro ao alterar status de banimento.' });
|
|
}
|
|
};
|
|
|
|
export const heartbeat = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
if (!req.appUser) return res.status(401).end();
|
|
await query('UPDATE users SET last_seen_at = NOW() WHERE id = $1', [req.appUser.id]);
|
|
res.status(200).send();
|
|
} catch (error) {
|
|
console.error('Heartbeat error:', error);
|
|
res.status(500).send();
|
|
}
|
|
};
|
|
|
|
export const getActiveUsers = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const organizationId = req.headers['x-organization-id'] as string;
|
|
if (!organizationId) return res.json([]);
|
|
|
|
const result = await query(
|
|
`SELECT u.id, u.name, u.email, u.last_seen_at, u.logto_id
|
|
FROM users u
|
|
JOIN user_organizations uo ON u.id = uo.user_id
|
|
WHERE uo.organization_id = $1
|
|
AND u.last_seen_at > NOW() - INTERVAL '2 minutes'
|
|
AND u.id != $2`,
|
|
[organizationId, req.appUser?.id]
|
|
);
|
|
|
|
res.json((result?.rows || []).map(snakeToCamel));
|
|
} catch (error) {
|
|
console.error('Error getting active users:', error);
|
|
res.status(500).json([]);
|
|
}
|
|
};
|
|
|
|
export const deleteUser = async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const organizationId = req.headers['x-organization-id'] as string;
|
|
|
|
if (!organizationId) {
|
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
|
}
|
|
|
|
const result = await query(
|
|
'DELETE FROM user_organizations WHERE user_id = $1 AND organization_id = $2 RETURNING *',
|
|
[id, organizationId]
|
|
);
|
|
|
|
if (!result?.rowCount) {
|
|
return res.status(404).json({ error: 'Membro não encontrado.' });
|
|
}
|
|
|
|
res.json({ message: 'Membro removido com sucesso.' });
|
|
} catch (error) {
|
|
console.error('Error deleting user:', error);
|
|
res.status(500).json({ error: 'Erro ao remover membro.' });
|
|
}
|
|
};
|
|
|
|
|