feat: invitar a un proyecto por correo a alguien que aun no es usuario
Al agregar un miembro, el popup ahora permite elegir un usuario existente o escribir un correo nuevo. Si ese correo no pertenece a nadie, se crea una cuenta en estado 'invited' (nombre/clave placeholder) y se le manda un correo con un enlace de un solo uso para que complete su registro (elige su nombre y clave). La fila de project_members se crea de una vez al invitar, asi que en cuanto acepta ya es miembro sin pasos adicionales. A diferencia del autorregistro publico, no pasa por aprobacion de un admin -- quien lo agrego a su proyecto ya lo vetó. Nuevos endpoints publicos GET /api/auth/invite y POST /api/auth/accept-invite. Nueva pagina AcceptInvitePage (/aceptar-invitacion). Migracion 004 agrega el status 'invited' al enum de users. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { createNotification } from '../notifications/service.js';
|
||||
const STATUS_MESSAGES = {
|
||||
pending_verification: 'Debes confirmar tu correo antes de iniciar sesion. Revisa tu bandeja de entrada.',
|
||||
pending_approval: 'Tu cuenta esta pendiente de aprobacion por un administrador.',
|
||||
invited: 'Tienes una invitacion pendiente. Revisa tu correo para completar tu registro.',
|
||||
};
|
||||
|
||||
function newToken() {
|
||||
@@ -130,6 +131,57 @@ export async function verifyEmail(token) {
|
||||
return { email: user.email };
|
||||
}
|
||||
|
||||
// Info publica minima para prellenar el formulario de "aceptar invitacion" (solo el correo).
|
||||
export async function getInviteInfo(token) {
|
||||
if (!token) throw new HttpError(400, 'Token invalido');
|
||||
const [rows] = await pool.query(
|
||||
`SELECT email FROM users WHERE verification_token = ? AND status = 'invited' AND verification_token_expires > NOW()`,
|
||||
[token]
|
||||
);
|
||||
const user = rows[0];
|
||||
if (!user) throw new HttpError(400, 'La invitacion es invalida o expiro');
|
||||
return { email: user.email };
|
||||
}
|
||||
|
||||
// Completa la cuenta creada por una invitacion a un proyecto (ver modules/users/service.js
|
||||
// inviteUser): la persona elige su nombre y clave. A diferencia del autorregistro publico, no
|
||||
// pasa por aprobacion de un admin — quien la invito ya la vetó al agregarla a su proyecto. Devuelve
|
||||
// sesion iniciada de una vez, igual que el bootstrap del primer usuario.
|
||||
export async function acceptInvite(token, name, password) {
|
||||
if (!token || !name || !password || password.length < 8) {
|
||||
throw new HttpError(400, 'Nombre, token y una clave de al menos 8 caracteres son obligatorios');
|
||||
}
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id FROM users WHERE verification_token = ? AND status = 'invited' AND verification_token_expires > NOW()`,
|
||||
[token]
|
||||
);
|
||||
const user = rows[0];
|
||||
if (!user) throw new HttpError(400, 'La invitacion es invalida o expiro');
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
await pool.query(
|
||||
`UPDATE users SET name = ?, password_hash = ?, is_active = 1, status = 'active', email_verified_at = NOW(),
|
||||
verification_token = NULL, verification_token_expires = NULL
|
||||
WHERE id = ?`,
|
||||
[name, passwordHash, user.id]
|
||||
);
|
||||
await logAudit({ userId: user.id, action: 'accept_invite', entityType: 'user', entityId: user.id });
|
||||
|
||||
const [full] = await pool.query(
|
||||
`SELECT u.id, u.name, u.email, u.department_id, r.code AS role_code, r.name AS role_name
|
||||
FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = ?`,
|
||||
[user.id]
|
||||
);
|
||||
const jwtToken = jwt.sign({ sub: user.id }, env.jwtSecret, { expiresIn: env.jwtExpires });
|
||||
return {
|
||||
token: jwtToken,
|
||||
user: {
|
||||
id: full[0].id, name: full[0].name, email: full[0].email,
|
||||
roleCode: full[0].role_code, roleName: full[0].role_name, departmentId: full[0].department_id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function forgotPassword(email) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, name FROM users WHERE email = ? AND status = 'active' AND is_active = 1`,
|
||||
|
||||
Reference in New Issue
Block a user