mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {createOriginHandoff, redeemOriginHandoff} from '@app/api/auth/services/OriginHandoffService';
|
|
import {Config} from '@app/api/Config';
|
|
import {DefaultUserOnly, LoginRequired} from '@app/api/middleware/AuthMiddleware';
|
|
import {RateLimitMiddleware} from '@app/api/middleware/RateLimitMiddleware';
|
|
import {OpenAPI} from '@app/api/middleware/ResponseTypeMiddleware';
|
|
import {RateLimitConfigs} from '@app/api/RateLimitConfig';
|
|
import type {HonoApp} from '@app/api/types/HonoEnv';
|
|
import {Validator} from '@app/api/Validator';
|
|
import {FileSizeTooLargeError} from '@fluxer/errors/src/domains/core/FileSizeTooLargeError';
|
|
import {InvalidApiOriginError} from '@fluxer/errors/src/domains/core/InvalidApiOriginError';
|
|
import {
|
|
ORIGIN_HANDOFF_MAX_PAYLOAD_LENGTH,
|
|
OriginHandoffCreateRequest,
|
|
OriginHandoffCreateResponse,
|
|
OriginHandoffRedeemRequest,
|
|
OriginHandoffRedeemResponse,
|
|
} from '@fluxer/schema/src/domains/auth/OriginHandoffSchemas';
|
|
import {bodyLimit} from 'hono/body-limit';
|
|
|
|
const ORIGIN_HANDOFF_CREATE_MAX_BODY_BYTES = ORIGIN_HANDOFF_MAX_PAYLOAD_LENGTH + 1024;
|
|
|
|
export function OriginHandoffController(app: HonoApp) {
|
|
app.post(
|
|
'/auth/origin-handoff',
|
|
RateLimitMiddleware(RateLimitConfigs.AUTH_ORIGIN_HANDOFF_CREATE),
|
|
LoginRequired,
|
|
DefaultUserOnly,
|
|
bodyLimit({
|
|
maxSize: ORIGIN_HANDOFF_CREATE_MAX_BODY_BYTES,
|
|
onError: () => {
|
|
throw new FileSizeTooLargeError(ORIGIN_HANDOFF_CREATE_MAX_BODY_BYTES);
|
|
},
|
|
}),
|
|
Validator('json', OriginHandoffCreateRequest),
|
|
OpenAPI({
|
|
operationId: 'create_origin_handoff',
|
|
summary: 'Create origin handoff',
|
|
responseSchema: OriginHandoffCreateResponse,
|
|
statusCode: 200,
|
|
security: ['sessionToken'],
|
|
tags: ['Auth'],
|
|
description:
|
|
'Store encrypted client state for up to two minutes so another first-party web origin can redeem it once. The receiving origin must present the nonce whose SHA-256 digest is sent here.',
|
|
}),
|
|
async (ctx) => {
|
|
const body = ctx.req.valid('json');
|
|
const handoffId = await createOriginHandoff(ctx.get('cacheService'), {
|
|
userId: ctx.get('user').id,
|
|
nonceHash: body.nonce_hash,
|
|
payload: body.payload,
|
|
});
|
|
const response: OriginHandoffCreateResponse = {handoff_id: handoffId};
|
|
return ctx.json(response);
|
|
},
|
|
);
|
|
app.post(
|
|
'/auth/origin-handoff/redeem',
|
|
RateLimitMiddleware(RateLimitConfigs.AUTH_ORIGIN_HANDOFF_REDEEM),
|
|
Validator('json', OriginHandoffRedeemRequest),
|
|
OpenAPI({
|
|
operationId: 'redeem_origin_handoff',
|
|
summary: 'Redeem origin handoff',
|
|
responseSchema: OriginHandoffRedeemResponse,
|
|
statusCode: 200,
|
|
security: [],
|
|
tags: ['Auth'],
|
|
description:
|
|
'Return the encrypted client state stored by create origin handoff and delete it in the same step. A wrong nonce also consumes the handoff. On the official instance the request must come from a first-party web origin.',
|
|
}),
|
|
async (ctx) => {
|
|
if (!Config.instance.selfHosted) {
|
|
const origin = ctx.req.header('origin');
|
|
if (origin === undefined || !Config.endpoints.webAppOrigins.includes(origin)) {
|
|
throw new InvalidApiOriginError();
|
|
}
|
|
}
|
|
const body = ctx.req.valid('json');
|
|
const payload = await redeemOriginHandoff(ctx.get('cacheService'), {
|
|
handoffId: body.handoff_id,
|
|
nonce: body.nonce,
|
|
});
|
|
const response: OriginHandoffRedeemResponse = {payload};
|
|
return ctx.json(response);
|
|
},
|
|
);
|
|
}
|