mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 22:22:39 +02:00
POST /steam/link took a Steam id from the request body and linked it. A Steam id is public, so anyone could claim any account, its library, and the one free weekend that is keyed on it. Linking is now POST /steam/link/start, which returns a Steam sign-in URL tied to a one-time, ten-minute request, and GET /steam/link/callback, where the browser comes back. The callback accepts the id only if Steam confirms the assertion, the assertion was issued for exactly this callback, and the id is read from Steam's claimed identity URL in the one shape Steam issues it. The request is spent on use.
145 lines
4.3 KiB
TypeScript
145 lines
4.3 KiB
TypeScript
import type { Hyperdrive } from '@cloudflare/workers-types';
|
|
import { Env } from '@nestri/core/env';
|
|
import { ErrorCodes, VisibleError } from '@nestri/core/error';
|
|
import { Hono } from 'hono';
|
|
import { openAPISpecs } from 'hono-openapi';
|
|
import { cors } from 'hono/cors';
|
|
import { HTTPException } from 'hono/http-exception';
|
|
import { logger } from 'hono/logger';
|
|
import { type ContentfulStatusCode } from 'hono/utils/http-status';
|
|
|
|
import { auth } from './middleware/auth.js';
|
|
import { AccessTokenApi } from './routes/access-token.js';
|
|
import { BillingApi } from './routes/billing.js';
|
|
import { BoxApi } from './routes/box.js';
|
|
import { EnrolmentApi } from './routes/enrolment.js';
|
|
import { GameApi } from './routes/game.js';
|
|
import { IndexApi } from './routes/index.js';
|
|
import { InstallApi } from './routes/install.js';
|
|
import { LibraryApi } from './routes/library.js';
|
|
import { MachineApi } from './routes/machine.js';
|
|
import { OrganisationApi } from './routes/organisation.js';
|
|
import { SessionApi } from './routes/session.js';
|
|
import { SteamApi } from './routes/steam.js';
|
|
import { TeamApi } from './routes/team.js';
|
|
import { UserApi } from './routes/user.js';
|
|
import { WaitlistApi } from './routes/waitlist.js';
|
|
|
|
export const app = new Hono();
|
|
|
|
app
|
|
.use(logger())
|
|
.use(async (c, next) => {
|
|
c.header('Cache-Control', 'no-store');
|
|
return next();
|
|
})
|
|
.use(
|
|
cors({
|
|
origin: () => 'http://localhost:5173',
|
|
credentials: true
|
|
})
|
|
)
|
|
.use(auth);
|
|
|
|
const routes = app
|
|
.route('/', IndexApi.route)
|
|
.route('/', InstallApi.route)
|
|
.route('/user', UserApi.route)
|
|
.route('/steam', SteamApi.publicRoute)
|
|
.route('/steam', SteamApi.route)
|
|
.route('/library', LibraryApi.route)
|
|
.route('/games', GameApi.route)
|
|
.route('/billing', BillingApi.route)
|
|
.route('/organisation', OrganisationApi.route)
|
|
.route('/machine', MachineApi.route)
|
|
.route('/machine', SessionApi.machineRoute)
|
|
.route('/machine', EnrolmentApi.route)
|
|
.route('/team', TeamApi.route)
|
|
.route('/box', BoxApi.route)
|
|
.route('/session', SessionApi.route)
|
|
.route('/access-token', AccessTokenApi.route)
|
|
.route('/waitlist', WaitlistApi.route)
|
|
.onError((error, c) => {
|
|
if (error instanceof VisibleError) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('api error:', error);
|
|
return c.json(error.toResponse(), error.statusCode() as ContentfulStatusCode);
|
|
}
|
|
|
|
if (error instanceof HTTPException) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('http error:', error);
|
|
return c.json(
|
|
{
|
|
type: 'validation',
|
|
code: ErrorCodes.Validation.INVALID_PARAMETER,
|
|
message: 'Invalid request'
|
|
},
|
|
error.status
|
|
);
|
|
}
|
|
// eslint-disable-next-line no-console
|
|
console.error('unhandled error:', error);
|
|
return c.json(
|
|
{
|
|
type: 'internal',
|
|
code: ErrorCodes.Server.INTERNAL_ERROR,
|
|
message: 'Internal server error'
|
|
},
|
|
500
|
|
);
|
|
});
|
|
|
|
app.get(
|
|
'/doc',
|
|
openAPISpecs(routes, {
|
|
documentation: {
|
|
info: {
|
|
title: 'Nestri API',
|
|
description: 'API',
|
|
version: '0.0.1'
|
|
},
|
|
components: {
|
|
securitySchemes: {
|
|
Bearer: {
|
|
type: 'http',
|
|
scheme: 'bearer',
|
|
bearerFormat: 'JWT'
|
|
}
|
|
}
|
|
},
|
|
security: [{ Bearer: [] }]
|
|
}
|
|
})
|
|
);
|
|
|
|
/**
|
|
* Everything this app is handed, from a binding or from the environment.
|
|
*
|
|
* Two things here arrive one of two ways, and neither is a special case.
|
|
* `HYPERDRIVE` carries a connection string on a platform that pools
|
|
* connections for us, and `DATABASE_URL` says the same thing where nothing
|
|
* does. An `AUTH` binding is a route to the issuer that skips the internet,
|
|
* and `AUTH_INTERNAL_URL` is that route written out. Each pair is two
|
|
* spellings of one fact rather than two deployments, which is why nothing
|
|
* below branches on the runtime it is under.
|
|
*
|
|
* `AUTH_ISSUER_URL` is not part of either pair. It is the issuer's public
|
|
* *name*, it is required, and it is the same value however the issuer is
|
|
* reached — because it is what every token's `iss` claim is checked against.
|
|
*/
|
|
export type ApiEnv = {
|
|
AUTH?: { fetch: typeof fetch };
|
|
AUTH_ISSUER_URL?: string;
|
|
AUTH_INTERNAL_URL?: string;
|
|
HYPERDRIVE?: Hyperdrive;
|
|
DATABASE_URL?: string;
|
|
};
|
|
|
|
export default {
|
|
fetch(request: Request, env: ApiEnv, ctx?: ExecutionContext) {
|
|
Env.init(env as unknown as Record<string, unknown>);
|
|
return app.fetch(request, env, ctx);
|
|
}
|
|
};
|