mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Converted admin, auth, me and realtime routes.
This commit is contained in:
parent
6c9f1ad06b
commit
5205551ca8
25 changed files with 346 additions and 7 deletions
39
src/app/api/admin/users/route.ts
Normal file
39
src/app/api/admin/users/route.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { z } from 'zod';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { json, unauthorized } from 'lib/response';
|
||||
import { pagingParams } from 'lib/schema';
|
||||
import { canViewUsers } from 'lib/auth';
|
||||
import { getUsers } from 'queries/prisma/user';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
if (!(await canViewUsers(auth))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const users = await getUsers(
|
||||
{
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
websiteUser: {
|
||||
where: { deletedAt: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
query,
|
||||
);
|
||||
|
||||
return json(users);
|
||||
}
|
||||
87
src/app/api/admin/websites/route.ts
Normal file
87
src/app/api/admin/websites/route.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { z } from 'zod';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { json, unauthorized } from 'lib/response';
|
||||
import { pagingParams } from 'lib/schema';
|
||||
import { canViewAllWebsites } from 'lib/auth';
|
||||
import { getWebsites } from 'queries/prisma/website';
|
||||
import { ROLES } from 'lib/constants';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
if (!(await canViewAllWebsites(auth))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const { userId, includeOwnedTeams, includeAllTeams } = query;
|
||||
|
||||
const websites = await getWebsites(
|
||||
{
|
||||
where: {
|
||||
OR: [
|
||||
...(userId && [{ userId }]),
|
||||
...(userId && includeOwnedTeams
|
||||
? [
|
||||
{
|
||||
team: {
|
||||
deletedAt: null,
|
||||
teamUser: {
|
||||
some: {
|
||||
role: ROLES.teamOwner,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(userId && includeAllTeams
|
||||
? [
|
||||
{
|
||||
team: {
|
||||
deletedAt: null,
|
||||
teamUser: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
where: {
|
||||
deletedAt: null,
|
||||
},
|
||||
include: {
|
||||
teamUser: {
|
||||
where: {
|
||||
role: ROLES.teamOwner,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
query,
|
||||
);
|
||||
|
||||
return json(websites);
|
||||
}
|
||||
44
src/app/api/auth/login/route.ts
Normal file
44
src/app/api/auth/login/route.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { z } from 'zod';
|
||||
import { checkPassword, createSecureToken } from 'next-basics';
|
||||
import { redisEnabled } from '@umami/redis-client';
|
||||
import { getUserByUsername } from 'queries';
|
||||
import { json, unauthorized } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { saveAuth } from 'lib/auth';
|
||||
import { secret } from 'lib/crypto';
|
||||
import { ROLES } from 'lib/constants';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
const { body, error } = await parseRequest(request, schema, { skipAuth: true });
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { username, password } = body;
|
||||
|
||||
const user = await getUserByUsername(username, { includePassword: true });
|
||||
|
||||
if (!user || !checkPassword(password, user.password)) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
if (redisEnabled) {
|
||||
const token = await saveAuth({ userId: user.id });
|
||||
|
||||
return json({ token, user });
|
||||
}
|
||||
|
||||
const token = createSecureToken({ userId: user.id }, secret());
|
||||
const { id, role, createdAt } = user;
|
||||
|
||||
return json({
|
||||
token,
|
||||
user: { id, username, role, createdAt, isAdmin: role === ROLES.admin },
|
||||
});
|
||||
}
|
||||
14
src/app/api/auth/logout/route.ts
Normal file
14
src/app/api/auth/logout/route.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { getClient, redisEnabled } from '@umami/redis-client';
|
||||
import { ok } from 'lib/response';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (redisEnabled) {
|
||||
const redis = getClient();
|
||||
|
||||
const token = request.headers.get('authorization')?.split(' ')?.[1];
|
||||
|
||||
await redis.del(token);
|
||||
}
|
||||
|
||||
return ok();
|
||||
}
|
||||
18
src/app/api/auth/sso/route.ts
Normal file
18
src/app/api/auth/sso/route.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { redisEnabled } from '@umami/redis-client';
|
||||
import { json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { saveAuth } from 'lib/auth';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { auth, error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
if (redisEnabled) {
|
||||
const token = await saveAuth({ userId: auth.user.id }, 86400);
|
||||
|
||||
return json({ user: auth.user, token });
|
||||
}
|
||||
}
|
||||
12
src/app/api/auth/verify/route.ts
Normal file
12
src/app/api/auth/verify/route.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { parseRequest } from 'lib/request';
|
||||
import { json } from 'lib/response';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { auth, error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
return json(auth.user);
|
||||
}
|
||||
33
src/app/api/me/password/route.ts
Normal file
33
src/app/api/me/password/route.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { z } from 'zod';
|
||||
import { parseRequest } from 'lib/request';
|
||||
import { json, badRequest } from 'lib/response';
|
||||
import { getUser, updateUser } from 'queries/prisma/user';
|
||||
import { checkPassword, hashPassword } from 'next-basics';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
currentPassword: z.string(),
|
||||
newPassword: z.string().min(8),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const userId = auth.user.id;
|
||||
const { currentPassword, newPassword } = body;
|
||||
|
||||
const user = await getUser(userId, { includePassword: true });
|
||||
|
||||
if (!checkPassword(currentPassword, user.password)) {
|
||||
return badRequest('Current password is incorrect');
|
||||
}
|
||||
|
||||
const password = hashPassword(newPassword);
|
||||
|
||||
const updated = await updateUser(userId, { password });
|
||||
|
||||
return json(updated);
|
||||
}
|
||||
12
src/app/api/me/route.ts
Normal file
12
src/app/api/me/route.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { parseRequest } from 'lib/request';
|
||||
import { json } from 'lib/response';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { auth, error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
return json(auth);
|
||||
}
|
||||
21
src/app/api/me/teams/route.ts
Normal file
21
src/app/api/me/teams/route.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { z } from 'zod';
|
||||
import { pagingParams } from 'lib/schema';
|
||||
import { getUserTeams } from 'queries';
|
||||
import { json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const teams = await getUserTeams(auth.user.id, query);
|
||||
|
||||
return json(teams);
|
||||
}
|
||||
21
src/app/api/me/websites/route.ts
Normal file
21
src/app/api/me/websites/route.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { z } from 'zod';
|
||||
import { pagingParams } from 'lib/schema';
|
||||
import { getUserWebsites } from 'queries';
|
||||
import { json } from 'lib/response';
|
||||
import { parseRequest } from 'lib/request';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const websites = await getUserWebsites(auth.user.id, query);
|
||||
|
||||
return json(websites);
|
||||
}
|
||||
30
src/app/api/realtime/[websiteId]/route.ts
Normal file
30
src/app/api/realtime/[websiteId]/route.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { json, unauthorized } from 'lib/response';
|
||||
import { getRealtimeData } from 'queries';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { startOfMinute, subMinutes } from 'date-fns';
|
||||
import { REALTIME_RANGE } from 'lib/constants';
|
||||
import { parseRequest } from 'lib/request';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const { auth, query, error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { timezone } = query;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const startDate = subMinutes(startOfMinute(new Date()), REALTIME_RANGE);
|
||||
|
||||
const data = await getRealtimeData(websiteId, { startDate, timezone });
|
||||
|
||||
return json(data);
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
|
|||
|
||||
const { userId } = await params;
|
||||
|
||||
if (!auth.user.isAdmin && (!userId || auth.user.id !== userId)) {
|
||||
if (auth.user.id !== userId && !auth.user.isAdmin) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ export async function getJsonBody(request: Request) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function parseRequest(request: Request, schema?: ZodObject<any>) {
|
||||
export async function parseRequest(
|
||||
request: Request,
|
||||
schema?: ZodObject<any>,
|
||||
options?: { skipAuth: boolean },
|
||||
): Promise<any> {
|
||||
const url = new URL(request.url);
|
||||
let query = Object.fromEntries(url.searchParams);
|
||||
let body = await getJsonBody(request);
|
||||
|
|
@ -32,7 +36,7 @@ export async function parseRequest(request: Request, schema?: ZodObject<any>) {
|
|||
}
|
||||
}
|
||||
|
||||
const auth = !error ? await checkAuth(request) : null;
|
||||
const auth = !error && !options?.skipAuth ? await checkAuth(request) : null;
|
||||
|
||||
if (!error && !auth) {
|
||||
error = () => unauthorized();
|
||||
|
|
|
|||
|
|
@ -12,14 +12,18 @@ export function badRequest(message?: any) {
|
|||
return Response.json({ error: 'Bad request', message }, { status: 400 });
|
||||
}
|
||||
|
||||
export function notFound(message?: any) {
|
||||
return Response.json({ error: 'Not found', message, status: 404 });
|
||||
}
|
||||
|
||||
export function unauthorized(message?: any) {
|
||||
return Response.json({ error: 'Unauthorized', message }, { status: 401 });
|
||||
}
|
||||
|
||||
export function forbidden(message?: any) {
|
||||
return Response.json({ error: 'Forbidden', message, status: 403 });
|
||||
}
|
||||
|
||||
export function notFound(message?: any) {
|
||||
return Response.json({ error: 'Not found', message, status: 404 });
|
||||
}
|
||||
|
||||
export function serverError(error?: any) {
|
||||
return Response.json({ error: 'Server error', message: serializeError(error), status: 500 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue