mirror of
https://github.com/umami-software/umami.git
synced 2026-02-13 17:15:37 +01:00
Compare commits
6 commits
e5f794c329
...
d4ff7c8e3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ff7c8e3f | ||
|
|
6fd428683d | ||
|
|
a1efb2d86a | ||
|
|
8b2196b97a | ||
|
|
63d2bfe118 | ||
|
|
8f55ed9da9 |
13 changed files with 82 additions and 20 deletions
|
|
@ -3,6 +3,7 @@ CREATE TABLE "share" (
|
||||||
"share_id" UUID NOT NULL,
|
"share_id" UUID NOT NULL,
|
||||||
"entity_id" UUID NOT NULL,
|
"entity_id" UUID NOT NULL,
|
||||||
"share_type" INTEGER NOT NULL,
|
"share_type" INTEGER NOT NULL,
|
||||||
|
"name" VARCHAR(200) NOT NULL,
|
||||||
"slug" VARCHAR(100) NOT NULL,
|
"slug" VARCHAR(100) NOT NULL,
|
||||||
"parameters" JSONB NOT NULL,
|
"parameters" JSONB NOT NULL,
|
||||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
"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");
|
CREATE INDEX "share_entity_id_idx" ON "share"("entity_id");
|
||||||
|
|
||||||
-- MigrateData
|
-- 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(),
|
SELECT gen_random_uuid(),
|
||||||
website_id,
|
website_id,
|
||||||
|
name,
|
||||||
1,
|
1,
|
||||||
share_id,
|
share_id,
|
||||||
'{}'::jsonb,
|
'{}'::jsonb,
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,7 @@ model Board {
|
||||||
model Share {
|
model Share {
|
||||||
id String @id() @map("share_id") @db.Uuid
|
id String @id() @map("share_id") @db.Uuid
|
||||||
entityId String @map("entity_id") @db.Uuid
|
entityId String @map("entity_id") @db.Uuid
|
||||||
|
name String @db.VarChar(200)
|
||||||
shareType Int @map("share_type") @db.Integer
|
shareType Int @map("share_type") @db.Integer
|
||||||
slug String @unique() @db.VarChar(100)
|
slug String @unique() @db.VarChar(100)
|
||||||
parameters Json
|
parameters Json
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||||
|
import { messages } from '@/components/messages';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
|
|
||||||
export function UserAddForm({ onSave, onClose }) {
|
export function UserAddForm({ onSave, onClose }) {
|
||||||
|
|
@ -37,7 +38,10 @@ export function UserAddForm({ onSave, onClose }) {
|
||||||
<FormField
|
<FormField
|
||||||
label={formatMessage(labels.password)}
|
label={formatMessage(labels.password)}
|
||||||
name="password"
|
name="password"
|
||||||
rules={{ required: formatMessage(labels.required) }}
|
rules={{
|
||||||
|
required: formatMessage(labels.required),
|
||||||
|
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: '8' }) },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PasswordField autoComplete="new-password" data-test="input-password" />
|
<PasswordField autoComplete="new-password" data-test="input-password" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import redis from '@/lib/redis';
|
import redis from '@/lib/redis';
|
||||||
|
import { parseRequest } from '@/lib/request';
|
||||||
import { ok } from '@/lib/response';
|
import { ok } from '@/lib/response';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
|
const { error } = await parseRequest(request);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
if (redis.enabled) {
|
if (redis.enabled) {
|
||||||
const token = request.headers.get('authorization')?.split(' ')?.[1];
|
const token = request.headers.get('authorization')?.split(' ')?.[1];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { saveAuth } from '@/lib/auth';
|
import { saveAuth } from '@/lib/auth';
|
||||||
import redis from '@/lib/redis';
|
import redis from '@/lib/redis';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { json } from '@/lib/response';
|
import { json, serverError } from '@/lib/response';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const { auth, error } = await parseRequest(request);
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
@ -10,9 +10,13 @@ export async function POST(request: Request) {
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redis.enabled) {
|
if (!redis.enabled) {
|
||||||
const token = await saveAuth({ userId: auth.user.id }, 86400);
|
return serverError({
|
||||||
|
message: 'Redis is disabled',
|
||||||
return json({ user: auth.user, token });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const token = await saveAuth({ userId: auth.user.id }, 86400);
|
||||||
|
|
||||||
|
return json({ user: auth.user, token });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { hashPassword } from '@/lib/password';
|
import { hashPassword } from '@/lib/password';
|
||||||
import { parseRequest } from '@/lib/request';
|
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 { userRoleParam } from '@/lib/schema';
|
||||||
import { canDeleteUser, canUpdateUser, canViewUser } from '@/permissions';
|
import { canDeleteUser, canUpdateUser, canViewUser } from '@/permissions';
|
||||||
import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries/prisma';
|
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 }> }) {
|
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
username: z.string().max(255).optional(),
|
username: z.string().max(255).optional(),
|
||||||
password: z.string().max(255).optional(),
|
password: z.string().min(8).max(255).optional(),
|
||||||
role: userRoleParam.optional(),
|
role: userRoleParam.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -47,6 +47,10 @@ export async function POST(request: Request, { params }: { params: Promise<{ use
|
||||||
|
|
||||||
const user = await getUser(userId);
|
const user = await getUser(userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
const data: any = {};
|
const data: any = {};
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { uuid } from '@/lib/crypto';
|
||||||
import { hashPassword } from '@/lib/password';
|
import { hashPassword } from '@/lib/password';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { badRequest, json, unauthorized } from '@/lib/response';
|
import { badRequest, json, unauthorized } from '@/lib/response';
|
||||||
|
import { userRoleParam } from '@/lib/schema';
|
||||||
import { canCreateUser } from '@/permissions';
|
import { canCreateUser } from '@/permissions';
|
||||||
import { createUser, getUserByUsername } from '@/queries/prisma';
|
import { createUser, getUserByUsername } from '@/queries/prisma';
|
||||||
|
|
||||||
|
|
@ -11,8 +12,8 @@ export async function POST(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
id: z.uuid().optional(),
|
id: z.uuid().optional(),
|
||||||
username: z.string().max(255),
|
username: z.string().max(255),
|
||||||
password: z.string(),
|
password: z.string().min(8).max(255),
|
||||||
role: z.string().regex(/admin|user|view-only/i),
|
role: userRoleParam,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
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 { canUpdateWebsite, canViewWebsite } from '@/permissions';
|
||||||
import { createSegment, getWebsiteSegments } from '@/queries/prisma';
|
import { createSegment, getWebsiteSegments } from '@/queries/prisma';
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ export async function POST(
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
type: segmentTypeParam,
|
type: segmentTypeParam,
|
||||||
name: z.string().max(200),
|
name: z.string().max(200),
|
||||||
parameters: anyObjectParam,
|
parameters: segmentParametersSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants';
|
import { ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants';
|
||||||
import { secret } from '@/lib/crypto';
|
import { createAuthKey, secret } from '@/lib/crypto';
|
||||||
import { getRandomChars } from '@/lib/generate';
|
|
||||||
import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt';
|
import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt';
|
||||||
import redis from '@/lib/redis';
|
import redis from '@/lib/redis';
|
||||||
import { ensureArray } from '@/lib/utils';
|
import { ensureArray } from '@/lib/utils';
|
||||||
|
|
@ -53,7 +52,7 @@ export async function checkAuth(request: Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveAuth(data: any, expire = 0) {
|
export async function saveAuth(data: any, expire = 0) {
|
||||||
const authKey = `auth:${getRandomChars(32)}`;
|
const authKey = `auth:${createAuthKey()}`;
|
||||||
|
|
||||||
if (redis.enabled) {
|
if (redis.enabled) {
|
||||||
await redis.client.set(authKey, data);
|
await redis.client.set(authKey, data);
|
||||||
|
|
|
||||||
|
|
@ -63,3 +63,7 @@ export function uuid(...args: any) {
|
||||||
|
|
||||||
return process.env.USE_UUIDV7 ? v7() : v4();
|
return process.env.USE_UUIDV7 ? v7() : v4();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createAuthKey() {
|
||||||
|
return crypto.randomBytes(16).toString('hex');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,23 @@ export const reportTypeParam = z.enum([
|
||||||
'utm',
|
'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({
|
export const goalReportSchema = z.object({
|
||||||
type: z.literal('goal'),
|
type: z.literal('goal'),
|
||||||
parameters: z
|
parameters: z
|
||||||
|
|
@ -157,7 +174,7 @@ export const retentionReportSchema = z.object({
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
startDate: z.coerce.date(),
|
startDate: z.coerce.date(),
|
||||||
endDate: 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(),
|
startDate: z.coerce.date(),
|
||||||
endDate: z.coerce.date(),
|
endDate: z.coerce.date(),
|
||||||
unit: unitParam.optional(),
|
unit: unitParam.optional(),
|
||||||
timezone: z.string().optional(),
|
timezone: timezoneParam.optional(),
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -231,3 +248,22 @@ export const reportResultSchema = z.intersection(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const segmentTypeParam = z.enum(['segment', 'cohort']);
|
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(),
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export async function canCreateTeam({ user }: Auth) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!user;
|
return hasPermission(user.role, PERMISSIONS.teamCreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ async function findUser(criteria: Prisma.UserFindUniqueArgs, options: GetUserOpt
|
||||||
...criteria,
|
...criteria,
|
||||||
where: {
|
where: {
|
||||||
...criteria.where,
|
...criteria.where,
|
||||||
...(showDeleted && { deletedAt: null }),
|
...(showDeleted ? {} : { deletedAt: null }),
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue