Convert /api/users.

This commit is contained in:
Mike Cao 2025-01-21 19:10:34 -08:00
parent 090abcff81
commit baa3851fb4
61 changed files with 1064 additions and 70 deletions

View file

@ -1,16 +1,64 @@
import { Report } from '@prisma/client';
import { getClient } from '@umami/redis-client';
import { getClient, redisEnabled } from '@umami/redis-client';
import debug from 'debug';
import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants';
import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from 'lib/constants';
import { secret } from 'lib/crypto';
import { NextApiRequest } from 'next';
import { createSecureToken, ensureArray, getRandomChars, parseToken } from 'next-basics';
import { getTeamUser, getWebsite } from 'queries';
import {
createSecureToken,
ensureArray,
getRandomChars,
parseSecureToken,
parseToken,
} from 'next-basics';
import { getTeamUser, getUser, getWebsite } from 'queries';
import { Auth } from './types';
const log = debug('umami:auth');
const cloudMode = process.env.CLOUD_MODE;
export async function checkAuth(request: Request) {
const token = request.headers.get('authorization')?.split(' ')?.[1];
const payload = parseSecureToken(token, secret());
const shareToken = await parseShareToken(request as any);
let user = null;
const { userId, authKey, grant } = payload || {};
if (userId) {
user = await getUser(userId);
} else if (redisEnabled && authKey) {
const redis = getClient();
const key = await redis.get(authKey);
if (key?.userId) {
user = await getUser(key.userId);
}
}
if (process.env.NODE_ENV === 'development') {
log('checkAuth:', { token, shareToken, payload, user, grant });
}
if (!user?.id && !shareToken) {
log('checkAuth: User not authorized');
return null;
}
if (user) {
user.isAdmin = user.role === ROLES.admin;
}
return {
user,
grant,
token,
shareToken,
authKey,
};
}
export async function saveAuth(data: any, expire = 0) {
const authKey = `auth:${getRandomChars(32)}`;

View file

@ -1,10 +1,28 @@
import { NextApiRequest } from 'next';
import { ZodObject } from 'zod';
import { getAllowedUnits, getMinimumUnit } from './date';
import { getWebsiteDateRange } from '../queries';
import { FILTER_COLUMNS } from 'lib/constants';
export async function getRequestDateRange(req: NextApiRequest) {
const { websiteId, startAt, endAt, unit } = req.query;
export async function getJsonBody(request: Request) {
try {
return await request.clone().json();
} catch {
return null;
}
}
export async function checkRequest(request: Request, schema: ZodObject<any>) {
const url = new URL(request.url);
const query = Object.fromEntries(url.searchParams);
const body = await getJsonBody(request);
const result = schema.safeParse(request.method === 'GET' ? query : body);
return { query, body, error: result.error };
}
export async function getRequestDateRange(query: Record<string, any>) {
const { websiteId, startAt, endAt, unit } = query;
// All-time
if (+startAt === 0 && +endAt === 1) {
@ -31,9 +49,9 @@ export async function getRequestDateRange(req: NextApiRequest) {
};
}
export function getRequestFilters(req: NextApiRequest) {
export function getRequestFilters(query: Record<string, any>) {
return Object.keys(FILTER_COLUMNS).reduce((obj, key) => {
const value = req.query[key];
const value = query[key];
if (value !== undefined) {
obj[key] = value;

21
src/lib/response.ts Normal file
View file

@ -0,0 +1,21 @@
import { serializeError } from 'serialize-error';
export function ok() {
return Response.json({ ok: true });
}
export function json(data: any) {
return Response.json(data);
}
export function badRequest(message?: any) {
return Response.json({ error: 'Bad request', message }, { status: 400 });
}
export function unauthorized() {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
export function serverError(error: any) {
return Response.json({ error: 'Server error', message: serializeError(error), status: 500 });
}

View file

@ -1,4 +1,7 @@
import { z } from 'zod';
import * as yup from 'yup';
import { isValidTimezone } from 'lib/date';
import { UNIT_TYPES } from './constants';
export const dateRange = {
startAt: yup.number().integer().required(),
@ -11,3 +14,18 @@ export const pageInfo = {
pageSize: yup.number().integer().positive().min(1).max(200),
orderBy: yup.string(),
};
export const pagingParams = {
page: z.coerce.number().int().positive(),
pageSize: z.coerce.number().int().positive(),
orderBy: z.string().optional(),
query: z.string().optional(),
};
export const timezone = z.string().refine(value => isValidTimezone(value), {
message: 'Invalid timezone',
});
export const unit = z.string().refine(value => UNIT_TYPES.includes(value), {
message: 'Invalid unit',
});

View file

@ -25,7 +25,7 @@ export type KafkaTopic = ObjectValues<typeof KAFKA_TOPIC>;
export type ReportType = ObjectValues<typeof REPORT_TYPES>;
export interface PageParams {
query?: string;
search?: string;
page?: number;
pageSize?: number;
orderBy?: string;
@ -43,7 +43,7 @@ export interface PageResult<T> {
export interface PagedQueryResult<T> {
result: PageResult<T>;
query: any;
search: any;
params: PageParams;
setParams: Dispatch<SetStateAction<T | PageParams>>;
}