This commit is contained in:
Maieul BOYER 2025-12-01 19:26:22 +01:00 committed by apetitco
parent 79c7edb30f
commit 3140da7bab
30 changed files with 1148 additions and 95 deletions

View file

@ -14,6 +14,7 @@ const kRouteAuthDone = Symbol('shared-route-auth-done');
type AuthedUser = {
id: UserId;
name: string;
guest: boolean;
};
declare module 'fastify' {
@ -118,7 +119,7 @@ export const authPlugin = fp<{ onlySchema?: boolean }>(async (fastify, { onlySch
.clearCookie('token', { path: '/' })
.makeResponse(401, 'notLoggedIn', 'auth.noUser');
}
req.authUser = { id: user.id, name: user.display_name };
req.authUser = { id: user.id, name: user.name, guest: user.guest };
}
catch {
return res

View file

@ -38,6 +38,32 @@
}
}
},
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"disableOtp.failure.guest"
]
}
}
}
}
}
},
"401": {
"description": "Default Response",
"content": {
@ -139,6 +165,32 @@
}
}
},
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"enableOtp.failure.guest"
]
}
}
}
}
}
},
"401": {
"description": "Default Response",
"content": {
@ -846,12 +898,12 @@
"payload": {
"type": "object",
"required": [
"url"
"secret"
],
"properties": {
"url": {
"secret": {
"type": "string",
"description": "The otp url to feed into a 2fa app"
"description": "The otp secret"
}
}
}

View file

@ -7,6 +7,7 @@ import { typeResponse, isNullish } from '@shared/utils';
export const DisableOtpRes = {
'200': typeResponse('success', 'disableOtp.success'),
'500': typeResponse('failure', 'disableOtp.failure.generic'),
'400': typeResponse('failure', 'disableOtp.failure.guest'),
};
@ -18,6 +19,13 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
async function(req, res) {
void res;
if (isNullish(req.authUser)) { return res.makeResponse(500, 'failure', 'disableOtp.failure.generic'); }
if (req.authUser.guest) {
return res.makeResponse(
400,
'failure',
'disableOtp.failure.guest',
);
}
this.db.deleteUserOtpSecret(req.authUser.id);
return res.makeResponse(200, 'success', 'disableOtp.success');
},

View file

@ -10,6 +10,7 @@ export const EnableOtpRes = {
url: Type.String({ description: 'The otp url to feed into a 2fa app' }),
}),
'401': typeResponse('failure', ['enableOtp.failure.noUser', 'enableOtp.failure.noSecret']),
'400': typeResponse('failure', ['enableOtp.failure.guest']),
};
export type EnableOtpRes = MakeStaticResponse<typeof EnableOtpRes>;
@ -21,6 +22,13 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
{ schema: { response: EnableOtpRes, operationId: 'enableOtp' }, config: { requireAuth: true } },
async function(req, res) {
if (isNullish(req.authUser)) { return res.makeResponse(403, 'failure', 'enableOtp.failure.noUser'); }
if (req.authUser.guest) {
return res.makeResponse(
400,
'failure',
'enableOtp.failure.guest',
);
}
const otpSecret = this.db.ensureUserOtpSecret(req.authUser!.id);
if (isNullish(otpSecret)) { return res.makeResponse(403, 'failure', 'enableOtp.failure.noSecret'); }

View file

@ -2,12 +2,12 @@ import { FastifyPluginAsync } from 'fastify';
import { Type } from 'typebox';
import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils';
import { Otp } from '@shared/auth';
export const StatusOtpRes = {
200: Type.Union([
typeResponse('success', 'statusOtp.success.enabled', { url: Type.String({ description: 'The otp url to feed into a 2fa app' }) }),
typeResponse('success', 'statusOtp.success.enabled', {
secret: Type.String({ description: 'The otp secret' }),
}),
typeResponse('success', 'statusOtp.success.disabled'),
]),
500: typeResponse('failure', 'statusOtp.failure.generic'),
@ -19,13 +19,32 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
void _opts;
fastify.get(
'/api/auth/statusOtp',
{ schema: { response: StatusOtpRes, operationId: 'statusOtp' }, config: { requireAuth: true } },
{
schema: { response: StatusOtpRes, operationId: 'statusOtp' },
config: { requireAuth: true },
},
async function(req, res) {
if (isNullish(req.authUser)) { return res.makeResponse(500, 'failure', 'statusOtp.failure.generic'); }
if (isNullish(req.authUser)) {
return res.makeResponse(
500,
'failure',
'statusOtp.failure.generic',
);
}
const otpSecret = this.db.getUserOtpSecret(req.authUser.id);
if (isNullish(otpSecret)) { return res.makeResponse(200, 'success', 'statusOtp.success.disabled'); }
const otp = new Otp({ secret: otpSecret });
return res.makeResponse(200, 'success', 'statusOtp.success.enabled', { url: otp.totpURL });
if (isNullish(otpSecret)) {
return res.makeResponse(
200,
'success',
'statusOtp.success.disabled',
);
}
return res.makeResponse(
200,
'success',
'statusOtp.success.enabled',
{ secret: otpSecret },
);
},
);
};

View file

@ -51,6 +51,32 @@
}
}
},
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"disableOtp.failure.guest"
]
}
}
}
}
}
},
"401": {
"description": "Default Response",
"content": {
@ -155,6 +181,32 @@
}
}
},
"400": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failure"
]
},
"msg": {
"enum": [
"enableOtp.failure.guest"
]
}
}
}
}
}
},
"401": {
"description": "Default Response",
"content": {
@ -883,12 +935,12 @@
"payload": {
"type": "object",
"required": [
"url"
"secret"
],
"properties": {
"url": {
"secret": {
"type": "string",
"description": "The otp url to feed into a 2fa app"
"description": "The otp secret"
}
}
}
@ -1069,6 +1121,20 @@
},
"guest": {
"type": "boolean"
},
"selfInfo": {
"type": "object",
"properties": {
"login_name": {
"type": "string"
},
"provider_id": {
"type": "string"
},
"provider_user": {
"type": "string"
}
}
}
}
}

View file

@ -72,6 +72,20 @@
},
"guest": {
"type": "boolean"
},
"selfInfo": {
"type": "object",
"properties": {
"login_name": {
"type": "string"
},
"provider_id": {
"type": "string"
},
"provider_user": {
"type": "string"
}
}
}
}
}

View file

@ -7,6 +7,11 @@ import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils';
export const UserInfoRes = {
'200': typeResponse('success', 'userinfo.success', {
name: Type.String(), id: Type.String(), guest: Type.Boolean(),
selfInfo: Type.Optional(Type.Object({
login_name: Type.Optional(Type.String()),
provider_id: Type.Optional(Type.String()),
provider_user: Type.Optional(Type.String()),
})),
}),
'403': typeResponse('failure', 'userinfo.failure.notLoggedIn'),
'404': typeResponse('failure', 'userinfo.failure.unknownUser'),
@ -38,6 +43,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
if (req.params.user === 'me') {
req.params.user = req.authUser.id;
}
const askSelf = req.params.user === req.authUser.id;
const user = this.db.getUser(req.params.user);
if (isNullish(user)) {
@ -57,6 +63,11 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
// ```
// is the same as `val = !!something`
guest: !!user.guest,
selfInfo: askSelf ? {
login_name: user.login,
provider_id: user.provider_name,
provider_user: user.provider_unique,
} : null,
};
return res.makeResponse(200, 'success', 'userinfo.success', payload);