Skip to content

Commit

Permalink
Add api validations.
Browse files Browse the repository at this point in the history
  • Loading branch information
briancao committed Aug 20, 2023
1 parent 7d5a240 commit 7a7233e
Show file tree
Hide file tree
Showing 41 changed files with 686 additions and 176 deletions.
33 changes: 23 additions & 10 deletions lib/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import redis from '@umami/redis-client';
import cors from 'cors';
import debug from 'debug';
import { getAuthToken, parseShareToken } from 'lib/auth';
import { ROLES } from 'lib/constants';
import { isUuid, secret } from 'lib/crypto';
import { findSession } from 'lib/session';
import {
createMiddleware,
unauthorized,
badRequest,
createMiddleware,
parseSecureToken,
tooManyRequest,
unauthorized,
} from 'next-basics';
import debug from 'debug';
import cors from 'cors';
import redis from '@umami/redis-client';
import { findSession } from 'lib/session';
import { getAuthToken, parseShareToken } from 'lib/auth';
import { secret, isUuid } from 'lib/crypto';
import { ROLES } from 'lib/constants';
import { getUserById } from '../queries';
import { NextApiRequestCollect } from 'pages/api/send';
import { getUserById } from '../queries';
import { NextApiRequestQueryBody } from './types';

const log = debug('umami:middleware');

Expand Down Expand Up @@ -75,3 +76,15 @@ export const useAuth = createMiddleware(async (req, res, next) => {

next();
});

export const useValidate = createMiddleware(async (req: any, res, next) => {
try {
const { yup } = req as NextApiRequestQueryBody;

yup[req.method].validateSync({ ...req.query, ...req.body });
} catch (e: any) {
return badRequest(res, e.message);
}

next();
});
17 changes: 14 additions & 3 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
EVENT_TYPE,
KAFKA_TOPIC,
REPORT_FILTER_TYPES,
REPORT_TYPES,
ROLES,
TEAM_FILTER_TYPES,
USER_FILTER_TYPES,
WEBSITE_FILTER_TYPES,
} from './constants';
import * as yup from 'yup';

type ObjectValues<T> = T[keyof T];

Expand All @@ -18,6 +20,8 @@ export type Role = ObjectValues<typeof ROLES>;
export type EventType = ObjectValues<typeof EVENT_TYPE>;
export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
export type KafkaTopic = ObjectValues<typeof KAFKA_TOPIC>;
export type ReportType = ObjectValues<typeof REPORT_TYPES>;

export type ReportSearchFilterType = ObjectValues<typeof REPORT_FILTER_TYPES>;
export type UserSearchFilterType = ObjectValues<typeof USER_FILTER_TYPES>;
export type WebsiteSearchFilterType = ObjectValues<typeof WEBSITE_FILTER_TYPES>;
Expand Down Expand Up @@ -47,8 +51,8 @@ export interface ReportSearchFilter extends SearchFilter<ReportSearchFilterType>
export interface SearchFilter<T> {
filter?: string;
filterType?: T;
pageSize?: number;
page?: number;
pageSize: number;
page: number;
orderBy?: string;
}

Expand Down Expand Up @@ -76,11 +80,19 @@ export interface Auth {
};
}

export interface YupRequest {
GET?: yup.ObjectSchema<any>;
POST?: yup.ObjectSchema<any>;
PUT?: yup.ObjectSchema<any>;
DELETE?: yup.ObjectSchema<any>;
}

export interface NextApiRequestQueryBody<TQuery = any, TBody = any> extends NextApiRequest {
auth?: Auth;
query: TQuery & { [key: string]: string | string[] };
body: TBody;
headers: any;
yup: YupRequest;
}

export interface NextApiRequestAuth extends NextApiRequest {
Expand Down Expand Up @@ -168,7 +180,6 @@ export interface RealtimeUpdate {
export interface DateRange {
startDate: Date;
endDate: Date;
unit: string;
value: string;
}

Expand Down
19 changes: 19 additions & 0 deletions lib/yup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as yup from 'yup';

export function getDateRangeValidation() {
return {
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
};
}

// ex: /funnel|insights|retention/i
export function getFilterValidation(matchRegex) {
return {
filter: yup.string(),
filterType: yup.string().matches(matchRegex),
pageSize: yup.number().integer().positive().max(200),
page: yup.number().integer().positive(),
orderBy: yup.string(),
};
}
31 changes: 19 additions & 12 deletions pages/api/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import redis from '@umami/redis-client';
import debug from 'debug';
import { setAuthKey } from 'lib/auth';
import { secret } from 'lib/crypto';
import { useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, User } from 'lib/types';
import { NextApiResponse } from 'next';
import {
ok,
unauthorized,
badRequest,
checkPassword,
createSecureToken,
methodNotAllowed,
forbidden,
methodNotAllowed,
ok,
unauthorized,
} from 'next-basics';
import redis from '@umami/redis-client';
import { getUserByUsername } from 'queries';
import { secret } from 'lib/crypto';
import { NextApiRequestQueryBody, User } from 'lib/types';
import { setAuthKey } from 'lib/auth';
import * as yup from 'yup';

const log = debug('umami:auth');

Expand All @@ -27,6 +28,13 @@ export interface LoginResponse {
user: User;
}

const schema = {
POST: yup.object().shape({
username: yup.string().required(),
password: yup.string().required(),
}),
};

export default async (
req: NextApiRequestQueryBody<any, LoginRequestBody>,
res: NextApiResponse<LoginResponse>,
Expand All @@ -35,13 +43,12 @@ export default async (
return forbidden(res);
}

req.yup = schema;
await useValidate(req, res);

if (req.method === 'POST') {
const { username, password } = req.body;

if (!username || !password) {
return badRequest(res);
}

const user = await getUserByUsername(username, { includePassword: true });

if (user && checkPassword(password, user.password)) {
Expand Down
29 changes: 20 additions & 9 deletions pages/api/event-data/events.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import { canViewWebsite } from 'lib/auth';
import { useCors, useAuth } from 'lib/middleware';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getEventDataEvents } from 'queries';
import * as yup from 'yup';

export interface EventDataEventsRequestQuery {
export interface EventDataFieldsRequestQuery {
websiteId: string;
dateRange: {
startDate: string;
endDate: string;
};
event?: string;
startAt: string;
endAt: string;
event: string;
}

const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
event: yup.string().required(),
}),
};

export default async (
req: NextApiRequestQueryBody<EventDataEventsRequestQuery>,
req: NextApiRequestQueryBody<EventDataFieldsRequestQuery, any>,
res: NextApiResponse<any>,
) => {
await useCors(req, res);
await useAuth(req, res);

req.yup = schema;
await useValidate(req, res);

if (req.method === 'GET') {
const { websiteId, startAt, endAt, event } = req.query;

Expand Down
23 changes: 17 additions & 6 deletions pages/api/event-data/fields.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import { canViewWebsite } from 'lib/auth';
import { useCors, useAuth } from 'lib/middleware';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getEventDataFields } from 'queries';
import * as yup from 'yup';

export interface EventDataFieldsRequestQuery {
websiteId: string;
dateRange: {
startDate: string;
endDate: string;
};
startAt: string;
endAt: string;
field?: string;
}

const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
field: yup.string(),
}),
};

export default async (
req: NextApiRequestQueryBody<EventDataFieldsRequestQuery>,
res: NextApiResponse<any>,
) => {
await useCors(req, res);
await useAuth(req, res);

req.yup = schema;
await useValidate(req, res);

if (req.method === 'GET') {
const { websiteId, startAt, endAt, field } = req.query;

Expand Down
23 changes: 16 additions & 7 deletions pages/api/event-data/stats.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { canViewWebsite } from 'lib/auth';
import { useCors, useAuth } from 'lib/middleware';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { NextApiResponse } from 'next';
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
import { getEventDataStats } from 'queries';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import * as yup from 'yup';

export interface EventDataStatsRequestQuery {
websiteId: string;
dateRange: {
startDate: string;
endDate: string;
};
startAt: string;
endAt: string;
}

const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().integer().required(),
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
}),
};

export default async (
req: NextApiRequestQueryBody<EventDataStatsRequestQuery>,
res: NextApiResponse<any>,
) => {
await useCors(req, res);
await useAuth(req, res);

req.yup = schema;
await useValidate(req, res);

if (req.method === 'GET') {
const { websiteId, startAt, endAt } = req.query;

Expand Down
16 changes: 14 additions & 2 deletions pages/api/me/password.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { useAuth, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, User } from 'lib/types';
import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next';
import {
badRequest,
checkPassword,
forbidden,
hashPassword,
methodNotAllowed,
forbidden,
ok,
} from 'next-basics';
import { getUserById, updateUser } from 'queries';
import * as yup from 'yup';

export interface UserPasswordRequestQuery {
id: string;
Expand All @@ -20,6 +21,14 @@ export interface UserPasswordRequestBody {
newPassword: string;
}

const schema = {
POST: yup.object().shape({
id: yup.string().uuid().required(),
currentPassword: yup.string().required(),
newPassword: yup.string().min(8).required(),
}),
};

export default async (
req: NextApiRequestQueryBody<UserPasswordRequestQuery, UserPasswordRequestBody>,
res: NextApiResponse<User>,
Expand All @@ -30,6 +39,9 @@ export default async (

await useAuth(req, res);

req.yup = schema;
await useValidate(req, res);

const { currentPassword, newPassword } = req.body;
const { id } = req.auth.user;

Expand Down
Loading

0 comments on commit 7a7233e

Please sign in to comment.