Fix API lint errors blocking CI
eslint has been failing on 11 no-explicit-any errors, which kept the whole pipeline red — including the new publish job that waits on lint. The casts all worked around missing types rather than unknown shapes: - jwt: @fastify/jwt decorates the instance at runtime, so the namespace is not in FastifyInstance's type. Described the parts we call and cast through unknown once, in one place, instead of `as any` at five sites. - permissions: FastifyInstance.db is declared by the db plugin; the cast was stale and hid the real type. - paginate: the querystring schema already validates page/perPage, so the call sites now name that shape via PaginationQuery. Also dropped an unused import and an unused binding whose call is kept for its validation side effect. No behaviour change: eslint and tsc are both clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+30
-3
@@ -14,8 +14,33 @@ export interface RefreshTokenPayload {
|
|||||||
const ACCESS_TOKEN_EXPIRY = '15m';
|
const ACCESS_TOKEN_EXPIRY = '15m';
|
||||||
const REFRESH_TOKEN_EXPIRY = '7d';
|
const REFRESH_TOKEN_EXPIRY = '7d';
|
||||||
|
|
||||||
|
type JwtSign = (payload: object, options?: { expiresIn?: string }) => string;
|
||||||
|
type JwtVerify = (token: string) => unknown;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parts of the JWT decoration we actually call.
|
||||||
|
*
|
||||||
|
* @fastify/jwt decorates the instance at runtime and the refresh namespace is
|
||||||
|
* registered by our own auth plugin, so neither appears in FastifyInstance's
|
||||||
|
* type. Describing the shape here keeps the call sites type-checked instead of
|
||||||
|
* casting the instance to `any`, which switches checking off entirely.
|
||||||
|
*/
|
||||||
|
interface JwtDecoratedInstance {
|
||||||
|
jwt?: {
|
||||||
|
sign?: JwtSign;
|
||||||
|
verify?: JwtVerify;
|
||||||
|
refresh?: { sign?: JwtSign; verify?: JwtVerify };
|
||||||
|
jwtRefresh?: { sign?: JwtSign; verify?: JwtVerify };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The decorated JWT namespace, or undefined when the plugin is not loaded. */
|
||||||
|
export function getJwt(app: FastifyInstance): JwtDecoratedInstance['jwt'] {
|
||||||
|
return (app as unknown as JwtDecoratedInstance).jwt;
|
||||||
|
}
|
||||||
|
|
||||||
export function signAccessToken(app: FastifyInstance, payload: AccessTokenPayload): string {
|
export function signAccessToken(app: FastifyInstance, payload: AccessTokenPayload): string {
|
||||||
const signer = (app as any).jwt?.sign;
|
const signer = getJwt(app)?.sign;
|
||||||
if (typeof signer !== 'function') {
|
if (typeof signer !== 'function') {
|
||||||
throw new Error('JWT signer is not configured');
|
throw new Error('JWT signer is not configured');
|
||||||
}
|
}
|
||||||
@@ -23,7 +48,8 @@ export function signAccessToken(app: FastifyInstance, payload: AccessTokenPayloa
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function signRefreshToken(app: FastifyInstance, payload: RefreshTokenPayload): string {
|
export function signRefreshToken(app: FastifyInstance, payload: RefreshTokenPayload): string {
|
||||||
const signer = (app as any).jwt?.refresh?.sign ?? (app as any).jwt?.jwtRefresh?.sign;
|
const jwt = getJwt(app);
|
||||||
|
const signer = jwt?.refresh?.sign ?? jwt?.jwtRefresh?.sign;
|
||||||
if (typeof signer !== 'function') {
|
if (typeof signer !== 'function') {
|
||||||
throw new Error('Refresh JWT signer is not configured');
|
throw new Error('Refresh JWT signer is not configured');
|
||||||
}
|
}
|
||||||
@@ -31,7 +57,8 @@ export function signRefreshToken(app: FastifyInstance, payload: RefreshTokenPayl
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function verifyRefreshToken(app: FastifyInstance, token: string): RefreshTokenPayload {
|
export function verifyRefreshToken(app: FastifyInstance, token: string): RefreshTokenPayload {
|
||||||
const verifier = (app as any).jwt?.refresh?.verify ?? (app as any).jwt?.jwtRefresh?.verify;
|
const jwt = getJwt(app);
|
||||||
|
const verifier = jwt?.refresh?.verify ?? jwt?.jwtRefresh?.verify;
|
||||||
if (typeof verifier !== 'function') {
|
if (typeof verifier !== 'function') {
|
||||||
throw new Error('Refresh JWT verifier is not configured');
|
throw new Error('Refresh JWT verifier is not configured');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,16 @@ export const PaginationQuerySchema = Type.Object({
|
|||||||
perPage: Type.Optional(Type.Number({ minimum: 1, maximum: 100, default: 20 })),
|
perPage: Type.Optional(Type.Number({ minimum: 1, maximum: 100, default: 20 })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function paginate(query: { page?: number; perPage?: number }) {
|
/**
|
||||||
|
* The querystring shape PaginationQuerySchema validates.
|
||||||
|
*
|
||||||
|
* Route handlers receive `request.query` as `unknown`; the schema has already
|
||||||
|
* checked the values by then, so the cast at the call site is what tells
|
||||||
|
* TypeScript what Fastify handed over.
|
||||||
|
*/
|
||||||
|
export type PaginationQuery = { page?: number; perPage?: number };
|
||||||
|
|
||||||
|
export function paginate(query: PaginationQuery) {
|
||||||
const page = query.page ?? 1;
|
const page = query.page ?? 1;
|
||||||
const perPage = query.perPage ?? 20;
|
const perPage = query.perPage ?? 20;
|
||||||
const offset = (page - 1) * perPage;
|
const offset = (page - 1) * perPage;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function getOrgMembership(
|
|||||||
return 'super_admin';
|
return 'super_admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
const member = await (request.server as any).db.query.organizationMembers.findFirst({
|
const member = await request.server.db.query.organizationMembers.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(organizationMembers.organizationId, orgId),
|
eq(organizationMembers.organizationId, orgId),
|
||||||
eq(organizationMembers.userId, user.sub),
|
eq(organizationMembers.userId, user.sub),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Server as SocketIOServer } from 'socket.io';
|
|||||||
import { nodes, organizationMembers, servers } from '@source/database';
|
import { nodes, organizationMembers, servers } from '@source/database';
|
||||||
import { ROLES } from '@source/shared';
|
import { ROLES } from '@source/shared';
|
||||||
import type { Role } from '@source/shared';
|
import type { Role } from '@source/shared';
|
||||||
|
import { getJwt } from '../lib/jwt.js';
|
||||||
import type { AccessTokenPayload } from '../lib/jwt.js';
|
import type { AccessTokenPayload } from '../lib/jwt.js';
|
||||||
import {
|
import {
|
||||||
daemonOpenConsoleStream,
|
daemonOpenConsoleStream,
|
||||||
@@ -67,7 +68,7 @@ export default fp(async (app: FastifyInstance) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifier = (app as any).jwt?.verify;
|
const verifier = getJwt(app)?.verify;
|
||||||
if (typeof verifier !== 'function') {
|
if (typeof verifier !== 'function') {
|
||||||
next(new Error('Authentication is not configured'));
|
next(new Error('Authentication is not configured'));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { users, games, nodes, auditLogs, plugins, pluginReleases } from '@source
|
|||||||
import { AppError } from '../../lib/errors.js';
|
import { AppError } from '../../lib/errors.js';
|
||||||
import { requireSuperAdmin } from '../../lib/permissions.js';
|
import { requireSuperAdmin } from '../../lib/permissions.js';
|
||||||
import { paginate, paginatedResponse, PaginationQuerySchema } from '../../lib/pagination.js';
|
import { paginate, paginatedResponse, PaginationQuerySchema } from '../../lib/pagination.js';
|
||||||
|
import type { PaginationQuery } from '../../lib/pagination.js';
|
||||||
import { uploadPluginArtifact } from '../../lib/cdn.js';
|
import { uploadPluginArtifact } from '../../lib/cdn.js';
|
||||||
import * as yazl from 'yazl';
|
import * as yazl from 'yazl';
|
||||||
import {
|
import {
|
||||||
@@ -202,7 +203,7 @@ export default async function adminRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
// GET /api/admin/users
|
// GET /api/admin/users
|
||||||
app.get('/users', { schema: { querystring: PaginationQuerySchema } }, async (request) => {
|
app.get('/users', { schema: { querystring: PaginationQuerySchema } }, async (request) => {
|
||||||
const { page, perPage, offset, limit } = paginate(request.query as any);
|
const { page, perPage, offset, limit } = paginate(request.query as PaginationQuery);
|
||||||
|
|
||||||
const [totalResult] = await app.db.select({ count: count() }).from(users);
|
const [totalResult] = await app.db.select({ count: count() }).from(users);
|
||||||
|
|
||||||
@@ -912,7 +913,7 @@ export default async function adminRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
// GET /api/admin/audit-logs
|
// GET /api/admin/audit-logs
|
||||||
app.get('/audit-logs', { schema: { querystring: PaginationQuerySchema } }, async (request) => {
|
app.get('/audit-logs', { schema: { querystring: PaginationQuerySchema } }, async (request) => {
|
||||||
const { page, perPage, offset, limit } = paginate(request.query as any);
|
const { page, perPage, offset, limit } = paginate(request.query as PaginationQuery);
|
||||||
|
|
||||||
const [totalResult] = await app.db.select({ count: count() }).from(auditLogs);
|
const [totalResult] = await app.db.select({ count: count() }).from(auditLogs);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { eq } from 'drizzle-orm';
|
|||||||
import { users } from '@source/database';
|
import { users } from '@source/database';
|
||||||
import { hashPassword, verifyPassword } from '../../lib/password.js';
|
import { hashPassword, verifyPassword } from '../../lib/password.js';
|
||||||
import { signAccessToken, signRefreshToken, verifyRefreshToken } from '../../lib/jwt.js';
|
import { signAccessToken, signRefreshToken, verifyRefreshToken } from '../../lib/jwt.js';
|
||||||
import type { AccessTokenPayload, RefreshTokenPayload } from '../../lib/jwt.js';
|
import type { RefreshTokenPayload } from '../../lib/jwt.js';
|
||||||
import { AppError } from '../../lib/errors.js';
|
import { AppError } from '../../lib/errors.js';
|
||||||
import { RegisterSchema, LoginSchema } from './schemas.js';
|
import { RegisterSchema, LoginSchema } from './schemas.js';
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { organizations, organizationMembers, users } from '@source/database';
|
|||||||
import { AppError } from '../../lib/errors.js';
|
import { AppError } from '../../lib/errors.js';
|
||||||
import { requirePermission, getOrgMembership } from '../../lib/permissions.js';
|
import { requirePermission, getOrgMembership } from '../../lib/permissions.js';
|
||||||
import { paginate, paginatedResponse, PaginationQuerySchema } from '../../lib/pagination.js';
|
import { paginate, paginatedResponse, PaginationQuerySchema } from '../../lib/pagination.js';
|
||||||
|
import type { PaginationQuery } from '../../lib/pagination.js';
|
||||||
import { createAuditLog } from '../../lib/audit.js';
|
import { createAuditLog } from '../../lib/audit.js';
|
||||||
import {
|
import {
|
||||||
CreateOrgSchema,
|
CreateOrgSchema,
|
||||||
@@ -20,7 +21,7 @@ export default async function organizationRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
// GET /api/organizations — list user's organizations
|
// GET /api/organizations — list user's organizations
|
||||||
app.get('/', { schema: { querystring: PaginationQuerySchema } }, async (request) => {
|
app.get('/', { schema: { querystring: PaginationQuerySchema } }, async (request) => {
|
||||||
const { page, perPage, offset, limit } = paginate(request.query as any);
|
const { page, perPage, offset, limit } = paginate(request.query as PaginationQuery);
|
||||||
const userId = request.user.sub;
|
const userId = request.user.sub;
|
||||||
|
|
||||||
if (request.user.isSuperAdmin) {
|
if (request.user.isSuperAdmin) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { GameAutomationRule, PowerAction, ServerAutomationEvent } from '@so
|
|||||||
import { AppError } from '../../lib/errors.js';
|
import { AppError } from '../../lib/errors.js';
|
||||||
import { requirePermission } from '../../lib/permissions.js';
|
import { requirePermission } from '../../lib/permissions.js';
|
||||||
import { paginate, paginatedResponse, PaginationQuerySchema } from '../../lib/pagination.js';
|
import { paginate, paginatedResponse, PaginationQuerySchema } from '../../lib/pagination.js';
|
||||||
|
import type { PaginationQuery } from '../../lib/pagination.js';
|
||||||
import { createAuditLog } from '../../lib/audit.js';
|
import { createAuditLog } from '../../lib/audit.js';
|
||||||
import {
|
import {
|
||||||
deleteFivemQbCoreDatabase,
|
deleteFivemQbCoreDatabase,
|
||||||
@@ -637,7 +638,7 @@ export default async function serverRoutes(app: FastifyInstance) {
|
|||||||
const { orgId } = request.params as { orgId: string };
|
const { orgId } = request.params as { orgId: string };
|
||||||
await requirePermission(request, orgId, 'server.read');
|
await requirePermission(request, orgId, 'server.read');
|
||||||
|
|
||||||
const { page, perPage, offset, limit } = paginate(request.query as any);
|
const { page, perPage, offset, limit } = paginate(request.query as PaginationQuery);
|
||||||
|
|
||||||
const [totalResult] = await app.db
|
const [totalResult] = await app.db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
|
|||||||
@@ -874,7 +874,9 @@ export default async function pluginRoutes(app: FastifyInstance) {
|
|||||||
app.get('/', { schema: ParamSchema }, async (request) => {
|
app.get('/', { schema: ParamSchema }, async (request) => {
|
||||||
const { orgId, serverId } = request.params as { orgId: string; serverId: string };
|
const { orgId, serverId } = request.params as { orgId: string; serverId: string };
|
||||||
await requirePermission(request, orgId, 'plugin.read');
|
await requirePermission(request, orgId, 'plugin.read');
|
||||||
const context = await getServerPluginContext(app, orgId, serverId);
|
// Called for its validation of the server and the caller's access to it;
|
||||||
|
// this endpoint does not need the returned context.
|
||||||
|
await getServerPluginContext(app, orgId, serverId);
|
||||||
|
|
||||||
const installed = await app.db
|
const installed = await app.db
|
||||||
.select({
|
.select({
|
||||||
|
|||||||
Reference in New Issue
Block a user