diff --git a/prisma/migrations/15_add_share/migration.sql b/prisma/migrations/15_add_share/migration.sql index d9f1e7cf..3971b54c 100644 --- a/prisma/migrations/15_add_share/migration.sql +++ b/prisma/migrations/15_add_share/migration.sql @@ -3,6 +3,7 @@ CREATE TABLE "share" ( "share_id" UUID NOT NULL, "entity_id" UUID NOT NULL, "share_type" INTEGER NOT NULL, + "name" VARCHAR(200) NOT NULL, "slug" VARCHAR(100) NOT NULL, "parameters" JSONB NOT NULL, "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, @@ -21,9 +22,10 @@ CREATE UNIQUE INDEX "share_slug_key" ON "share"("slug"); CREATE INDEX "share_entity_id_idx" ON "share"("entity_id"); -- MigrateData -INSERT INTO "share" (share_id, entity_id, share_type, slug, parameters, created_at) +INSERT INTO "share" (share_id, entity_id, name, share_type, slug, parameters, created_at) SELECT gen_random_uuid(), website_id, + name, 1, share_id, '{}'::jsonb, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4da35406..e58ebd0b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -342,6 +342,7 @@ model Board { model Share { id String @id() @map("share_id") @db.Uuid entityId String @map("entity_id") @db.Uuid + name String @db.VarChar(200) shareType Int @map("share_type") @db.Integer slug String @unique() @db.VarChar(100) parameters Json diff --git a/src/app/(main)/admin/users/UserAddForm.tsx b/src/app/(main)/admin/users/UserAddForm.tsx index 6c365510..84b8399c 100644 --- a/src/app/(main)/admin/users/UserAddForm.tsx +++ b/src/app/(main)/admin/users/UserAddForm.tsx @@ -10,6 +10,7 @@ import { TextField, } from '@umami/react-zen'; import { useMessages, useUpdateQuery } from '@/components/hooks'; +import { messages } from '@/components/messages'; import { ROLES } from '@/lib/constants'; export function UserAddForm({ onSave, onClose }) { @@ -37,7 +38,10 @@ export function UserAddForm({ onSave, onClose }) { diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index 7bf0a813..153f1f52 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -1,7 +1,14 @@ import redis from '@/lib/redis'; +import { parseRequest } from '@/lib/request'; import { ok } from '@/lib/response'; export async function POST(request: Request) { + const { error } = await parseRequest(request); + + if (error) { + return error(); + } + if (redis.enabled) { const token = request.headers.get('authorization')?.split(' ')?.[1]; diff --git a/src/app/api/auth/sso/route.ts b/src/app/api/auth/sso/route.ts index bba3dde3..f8222869 100644 --- a/src/app/api/auth/sso/route.ts +++ b/src/app/api/auth/sso/route.ts @@ -1,7 +1,7 @@ import { saveAuth } from '@/lib/auth'; import redis from '@/lib/redis'; import { parseRequest } from '@/lib/request'; -import { json } from '@/lib/response'; +import { json, serverError } from '@/lib/response'; export async function POST(request: Request) { const { auth, error } = await parseRequest(request); @@ -10,9 +10,13 @@ export async function POST(request: Request) { return error(); } - if (redis.enabled) { - const token = await saveAuth({ userId: auth.user.id }, 86400); - - return json({ user: auth.user, token }); + if (!redis.enabled) { + return serverError({ + message: 'Redis is disabled', + }); } + + const token = await saveAuth({ userId: auth.user.id }, 86400); + + return json({ user: auth.user, token }); } diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts index aade8aa8..e642fe3c 100644 --- a/src/app/api/users/[userId]/route.ts +++ b/src/app/api/users/[userId]/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { hashPassword } from '@/lib/password'; import { parseRequest } from '@/lib/request'; -import { badRequest, json, ok, unauthorized } from '@/lib/response'; +import { badRequest, json, notFound, ok, unauthorized } from '@/lib/response'; import { userRoleParam } from '@/lib/schema'; import { canDeleteUser, canUpdateUser, canViewUser } from '@/permissions'; import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries/prisma'; @@ -27,7 +27,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) { const schema = z.object({ username: z.string().max(255).optional(), - password: z.string().max(255).optional(), + password: z.string().min(8).max(255).optional(), role: userRoleParam.optional(), }); @@ -47,6 +47,10 @@ export async function POST(request: Request, { params }: { params: Promise<{ use const user = await getUser(userId); + if (!user) { + return notFound(); + } + const data: any = {}; if (password) { diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index dbb114cf..4335c33f 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -4,6 +4,7 @@ import { uuid } from '@/lib/crypto'; import { hashPassword } from '@/lib/password'; import { parseRequest } from '@/lib/request'; import { badRequest, json, unauthorized } from '@/lib/response'; +import { userRoleParam } from '@/lib/schema'; import { canCreateUser } from '@/permissions'; import { createUser, getUserByUsername } from '@/queries/prisma'; @@ -11,8 +12,8 @@ export async function POST(request: Request) { const schema = z.object({ id: z.uuid().optional(), username: z.string().max(255), - password: z.string(), - role: z.string().regex(/admin|user|view-only/i), + password: z.string().min(8).max(255), + role: userRoleParam, }); const { auth, body, error } = await parseRequest(request, schema); diff --git a/src/app/api/websites/[websiteId]/segments/route.ts b/src/app/api/websites/[websiteId]/segments/route.ts index 45927656..10d47a81 100644 --- a/src/app/api/websites/[websiteId]/segments/route.ts +++ b/src/app/api/websites/[websiteId]/segments/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { anyObjectParam, searchParams, segmentTypeParam } from '@/lib/schema'; +import { searchParams, segmentParametersSchema, segmentTypeParam } from '@/lib/schema'; import { canUpdateWebsite, canViewWebsite } from '@/permissions'; import { createSegment, getWebsiteSegments } from '@/queries/prisma'; @@ -42,7 +42,7 @@ export async function POST( const schema = z.object({ type: segmentTypeParam, name: z.string().max(200), - parameters: anyObjectParam, + parameters: segmentParametersSchema, }); const { auth, body, error } = await parseRequest(request, schema); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index ba6d8b09..832dfb60 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,7 +1,6 @@ import debug from 'debug'; import { ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants'; -import { secret } from '@/lib/crypto'; -import { getRandomChars } from '@/lib/generate'; +import { createAuthKey, secret } from '@/lib/crypto'; import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt'; import redis from '@/lib/redis'; import { ensureArray } from '@/lib/utils'; @@ -53,7 +52,7 @@ export async function checkAuth(request: Request) { } export async function saveAuth(data: any, expire = 0) { - const authKey = `auth:${getRandomChars(32)}`; + const authKey = `auth:${createAuthKey()}`; if (redis.enabled) { await redis.client.set(authKey, data); diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index a6d912b8..ee4c977f 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -63,3 +63,7 @@ export function uuid(...args: any) { return process.env.USE_UUIDV7 ? v7() : v4(); } + +export function createAuthKey() { + return crypto.randomBytes(16).toString('hex'); +} diff --git a/src/lib/schema.ts b/src/lib/schema.ts index c860cee8..addce3a2 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -104,6 +104,23 @@ export const reportTypeParam = z.enum([ 'utm', ]); +export const operatorParam = z.enum([ + 'eq', + 'neq', + 's', + 'ns', + 'c', + 'dnc', + 't', + 'f', + 'gt', + 'lt', + 'gte', + 'lte', + 'bf', + 'af', +]); + export const goalReportSchema = z.object({ type: z.literal('goal'), parameters: z @@ -157,7 +174,7 @@ export const retentionReportSchema = z.object({ parameters: z.object({ startDate: z.coerce.date(), endDate: z.coerce.date(), - timezone: z.string().optional(), + timezone: timezoneParam.optional(), }), }); @@ -175,7 +192,7 @@ export const revenueReportSchema = z.object({ startDate: z.coerce.date(), endDate: z.coerce.date(), unit: unitParam.optional(), - timezone: z.string().optional(), + timezone: timezoneParam.optional(), currency: z.string(), }), }); @@ -231,3 +248,22 @@ export const reportResultSchema = z.intersection( ); export const segmentTypeParam = z.enum(['segment', 'cohort']); + +export const segmentParametersSchema = z.object({ + filters: z + .array( + z.object({ + name: z.string(), + operator: operatorParam, + value: z.string(), + }), + ) + .optional(), + dateRange: z.string().optional(), + action: z + .object({ + type: z.string(), + value: z.string(), + }) + .optional(), +}); diff --git a/src/permissions/team.ts b/src/permissions/team.ts index 0f07c1a4..784dbe4b 100644 --- a/src/permissions/team.ts +++ b/src/permissions/team.ts @@ -16,7 +16,7 @@ export async function canCreateTeam({ user }: Auth) { return true; } - return !!user; + return hasPermission(user.role, PERMISSIONS.teamCreate); } export async function canUpdateTeam({ user }: Auth, teamId: string) { diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts index 14376fc2..467ea1e0 100644 --- a/src/queries/prisma/user.ts +++ b/src/queries/prisma/user.ts @@ -18,7 +18,7 @@ async function findUser(criteria: Prisma.UserFindUniqueArgs, options: GetUserOpt ...criteria, where: { ...criteria.where, - ...(showDeleted && { deletedAt: null }), + ...(showDeleted ? {} : { deletedAt: null }), }, select: { id: true,