mirror of
https://github.com/umami-software/umami.git
synced 2026-02-12 08:37:13 +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;
|
const { userId } = await params;
|
||||||
|
|
||||||
if (!auth.user.isAdmin && (!userId || auth.user.id !== userId)) {
|
if (auth.user.id !== userId && !auth.user.isAdmin) {
|
||||||
return unauthorized();
|
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);
|
const url = new URL(request.url);
|
||||||
let query = Object.fromEntries(url.searchParams);
|
let query = Object.fromEntries(url.searchParams);
|
||||||
let body = await getJsonBody(request);
|
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) {
|
if (!error && !auth) {
|
||||||
error = () => unauthorized();
|
error = () => unauthorized();
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,18 @@ export function badRequest(message?: any) {
|
||||||
return Response.json({ error: 'Bad request', message }, { status: 400 });
|
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) {
|
export function unauthorized(message?: any) {
|
||||||
return Response.json({ error: 'Unauthorized', message }, { status: 401 });
|
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) {
|
export function serverError(error?: any) {
|
||||||
return Response.json({ error: 'Server error', message: serializeError(error), status: 500 });
|
return Response.json({ error: 'Server error', message: serializeError(error), status: 500 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue