File size: 6,060 Bytes
2ab1980
ef58703
2ab1980
 
30d60ea
2ab1980
 
 
 
 
a966957
2ab1980
c54affa
 
 
 
 
 
 
 
 
2ab1980
 
4e2a593
2ab1980
4e2a593
 
 
 
 
 
 
 
 
7b0c22b
2ab1980
 
4e2a593
2ab1980
 
 
 
 
 
 
 
 
 
 
 
 
 
a37afe6
2ab1980
 
 
 
 
 
 
 
 
7b0c22b
2ab1980
 
 
 
7b0c22b
a966957
7b0c22b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2ab1980
 
7b0c22b
2ab1980
30d60ea
2ab1980
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b0c22b
2ab1980
 
 
 
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
import { FastifyInstance } from 'fastify';

import { AuthService } from '../services/auth';
import { logger } from '../logger';
import { prisma } from '../services/prisma';

export async function authRoutes(fastify: FastifyInstance) {
    
    // Login with Email/Password
    fastify.post('/login', {
        config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
        schema: {
            body: {
                type: 'object',
                required: ['email', 'password'],
                properties: {
                    email:          { type: 'string', format: 'email' },
                    password:       { type: 'string' },
                    organizationId: { type: 'string' }
                }
            }
        }
    }, async (request, reply) => {
        const { email, password, organizationId: bodyOrgId } = request.body as { email: string; password: string; organizationId?: string };

        // If organizationId is omitted (older frontend), resolve it from the user record.
        // Security: password is still verified — this doesn't bypass auth.
        let user;
        if (bodyOrgId) {
            logger.info(`[AUTH] Login attempt for ${email} (Org: ${bodyOrgId})`);
            user = await AuthService.findUserByEmail(email, bodyOrgId);
        } else {
            logger.info(`[AUTH] Login attempt for ${email} (no org — resolving from email)`);
            user = await AuthService.findUserByEmailOnly(email);
        }
        
        if (!user || !user.passwordHash) {
            logger.warn(`[AUTH] User not found: ${email} in Org: ${bodyOrgId ?? 'unknown'}`);
            return reply.code(401).send({ error: 'Unauthorized', message: 'Invalid email or password' });
        }

        const isValid = await AuthService.verifyPassword(password, user.passwordHash);
        if (!isValid) {
            return reply.code(401).send({ error: 'Unauthorized', message: 'Invalid email or password' });
        }

        const token = fastify.jwt.sign({
            id: user.id,
            organizationId: user.organizationId,
            role: user.role
        });

        logger.info(`[AUTH] User ${email} logged in successfully for org ${user.organizationId}`);

        return {
            token,
            user: {
                id: user.id,
                name: user.name,
                email: user.email,
                role: user.role,
                organizationId: user.organizationId,
                organization: user.organization
            }
        };
    });

    // Request a password reset link
    fastify.post('/forgot-password', { config: { rateLimit: { max: 3, timeWindow: '1 minute' } } }, async (request, reply) => {
        const { email } = request.body as { email: string };
        if (!email) return reply.code(400).send({ error: 'Email required' });

        // Always return 200 to avoid email enumeration
        const user = await prisma.user.findFirst({ where: { email } });
        if (!user) return reply.send({ ok: true });

        // Sign a short-lived reset token (1 hour) with extra 'purpose' claim not in the standard payload shape
        const resetToken = (fastify.jwt.sign as Function)(
            { id: user.id, purpose: 'reset' },
            { expiresIn: '1h' }
        ) as string;

        const adminUrl = process.env.ADMIN_URL || 'https://edtechadmin.netlify.app';
        const resetLink = `${adminUrl}/reset-password?token=${resetToken}`;

        const { scheduleEmail } = await import('../services/queue');
        await scheduleEmail({
            to: email,
            subject: 'Réinitialisation de votre mot de passe — XAMLÉ',
            htmlContent: `<p>Bonjour ${user.name || ''},</p>
<p>Cliquez sur le lien ci-dessous pour réinitialiser votre mot de passe. Ce lien expire dans 1 heure.</p>
<p><a href="${resetLink}" style="background:#1e293b;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:bold;">Réinitialiser mon mot de passe</a></p>
<p>Si vous n'avez pas demandé cette réinitialisation, ignorez cet email.</p>`
        });

        logger.info({ email }, '[AUTH] Password reset link sent');
        return reply.send({ ok: true });
    });

    // Set new password via reset token
    fastify.post('/reset-password', async (request, reply) => {
        const { token, password } = request.body as { token: string; password: string };
        if (!token || !password) return reply.code(400).send({ error: 'Token and password required' });
        if (password.length < 6) return reply.code(400).send({ error: 'Password must be at least 6 characters' });

        let payload: { id: string; purpose?: string };
        try {
            payload = fastify.jwt.verify(token) as { id: string; purpose?: string };
        } catch {
            return reply.code(401).send({ error: 'Invalid or expired token' });
        }

        if (payload.purpose !== 'reset') {
            return reply.code(401).send({ error: 'Invalid token purpose' });
        }

        const passwordHash = await AuthService.hashPassword(password);
        await prisma.user.update({
            where: { id: payload.id },
            data: { passwordHash }
        });

        logger.info({ userId: payload.id }, '[AUTH] Password reset successfully');
        return reply.send({ ok: true });
    });

    // Get current user profile (guarded by JWT)
    fastify.get('/me', async (request, reply) => {
        const { id } = request.user;
        
        const user = await prisma.user.findUnique({
            where: { id },
            include: { organization: true }
        });

        if (!user) {
            return reply.code(404).send({ error: 'Not Found', message: 'User not found' });
        }

        return {
            user: {
                id: user.id,
                name: user.name,
                email: user.email,
                role: user.role,
                organizationId: user.organizationId,
                organization: user.organization
            }
        };
    });
}