Feat/um 114 roles and permissions (#1683)

* Auth checkpoint.

* Merge branch 'dev' into feat/um-114-roles-and-permissions
This commit is contained in:
Brian Cao 2022-12-01 20:53:37 -08:00 committed by GitHub
parent a4e80ca3e5
commit 06bebadbb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 331 additions and 482 deletions

View File

@ -287,10 +287,10 @@ INSERT INTO "user" (user_id, username, password) VALUES ('41e2b680-648e-4b09-bcd
-- Add Roles -- Add Roles
INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Admin', 'System Admin.'); INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Admin', 'System Admin.');
INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Member', 'Create and maintain websites.'); INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Member', 'Create and maintain websites.');
INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Team Owner', 'Create and maintain the team, memberships, websites, and responsible for billing.'); INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Team Owner', 'Create and maintain the team, memberships, websites, and responsible for billing.');
INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Team Member', 'Create and maintain websites.'); INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Team Member', 'Create and maintain websites.');
INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Team Guest', 'View Websites.'); INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Team Guest', 'View Websites.');
-- Add Permissions -- Add Permissions
INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'admin', 'System Admin'); INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'admin', 'System Admin');

View File

@ -14,11 +14,9 @@ model User {
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
isDeleted Boolean @default(false) @map("is_deleted") isDeleted Boolean @default(false) @map("is_deleted")
groupRole GroupRole[] userRole UserRole[]
groupUser GroupUser[] teamUser TeamUser[]
userRole UserRole[] Website Website[]
teamUser TeamUser[]
Website Website[]
@@map("user") @@map("user")
} }
@ -78,96 +76,16 @@ model WebsiteEvent {
@@map("website_event") @@map("website_event")
} }
model Group {
id String @id() @unique() @map("group_id") @db.Uuid
name String @unique() @db.VarChar(255)
description String? @db.VarChar(255)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
isDeleted Boolean @default(false) @map("is_deleted")
groupRoles GroupRole[]
groupUsers GroupUser[]
@@map("group")
}
model GroupRole {
id String @id() @unique() @map("group_role_id") @db.Uuid
groupId String @map("group_id") @db.Uuid
roleId String @map("role_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
isDeleted Boolean @default(false) @map("is_deleted")
group Group @relation(fields: [groupId], references: [id])
role Role @relation(fields: [roleId], references: [id])
user User? @relation(fields: [userId], references: [id])
userId String? @db.Uuid
@@map("group_role")
}
model GroupUser {
id String @id() @unique() @map("group_user_id") @db.Uuid
groupId String @map("group_id") @db.Uuid
userId String @map("user_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
isDeleted Boolean @default(false) @map("is_deleted")
group Group @relation(fields: [groupId], references: [id])
user User @relation(fields: [userId], references: [id])
@@map("group_user")
}
model Permission {
id String @id() @unique() @map("permission_id") @db.Uuid
name String @unique() @db.VarChar(255)
description String? @db.VarChar(255)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
RolePermission RolePermission[]
@@map("permission")
}
model Role {
id String @id() @unique() @map("role_id") @db.Uuid
name String @unique() @db.VarChar(255)
description String? @db.VarChar(255)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
groupRoles GroupRole[]
userRoles UserRole[]
RolePermission RolePermission[]
TeamUser TeamUser[]
@@map("role")
}
model RolePermission {
id String @id() @unique() @map("role_permission_id") @db.Uuid
roleId String @map("role_id") @db.Uuid
permissionId String @map("permission_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
isDeleted Boolean @default(false) @map("is_deleted")
role Role @relation(fields: [roleId], references: [id])
permission Permission @relation(fields: [permissionId], references: [id])
@@unique([roleId, permissionId])
@@map("role_permission")
}
model UserRole { model UserRole {
id String @id() @unique() @map("user_role_id") @db.Uuid id String @id() @unique() @map("user_role_id") @db.Uuid
roleId String @map("role_id") @db.Uuid role String @map("role") @db.VarChar(100)
userId String @map("user_id") @db.Uuid userId String @map("user_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
isDeleted Boolean @default(false) @map("is_deleted") isDeleted Boolean @default(false) @map("is_deleted")
role Role @relation(fields: [roleId], references: [id])
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
@@unique([roleId, userId]) @@unique([role, userId])
@@map("user_role") @@map("user_role")
} }
@ -187,13 +105,12 @@ model TeamUser {
id String @id() @unique() @map("team_user_id") @db.Uuid id String @id() @unique() @map("team_user_id") @db.Uuid
teamId String @map("team_id") @db.Uuid teamId String @map("team_id") @db.Uuid
userId String @map("user_id") @db.Uuid userId String @map("user_id") @db.Uuid
roleId String @map("role_id") @db.Uuid role String @map("role") @db.VarChar(100)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
isDeleted Boolean @default(false) @map("is_deleted") isDeleted Boolean @default(false) @map("is_deleted")
team Team @relation(fields: [teamId], references: [id]) team Team @relation(fields: [teamId], references: [id])
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
role Role @relation(fields: [roleId], references: [id])
@@map("team_user") @@map("team_user")
} }

View File

@ -1,10 +1,10 @@
import { UserRole } from '@prisma/client';
import debug from 'debug'; import debug from 'debug';
import { NextApiRequestAuth } from 'interface/api/nextApi';
import cache from 'lib/cache'; import cache from 'lib/cache';
import { SHARE_TOKEN_HEADER, UmamiApi } from 'lib/constants'; import { SHARE_TOKEN_HEADER, UmamiApi } from 'lib/constants';
import { secret } from 'lib/crypto'; import { secret } from 'lib/crypto';
import { parseSecureToken, parseToken } from 'next-basics'; import { parseSecureToken, parseToken } from 'next-basics';
import { getPermissionsByUserId, getTeamUser, getUser } from 'queries'; import { getTeamUser, getUserRoles } from 'queries';
const log = debug('umami:auth'); const log = debug('umami:auth');
@ -34,13 +34,6 @@ export function parseShareToken(req) {
} }
} }
export function hasPermission(
value: UmamiApi.Role | UmamiApi.Permission,
permissions: UmamiApi.Role[] | UmamiApi.Permission[],
) {
return permissions.some(a => a === value);
}
export function isValidToken(token, validation) { export function isValidToken(token, validation) {
try { try {
if (typeof validation === 'object') { if (typeof validation === 'object') {
@ -56,71 +49,6 @@ export function isValidToken(token, validation) {
return false; return false;
} }
export async function allowQuery(
req: NextApiRequestAuth,
type: UmamiApi.AuthType,
typeId?: string,
) {
const { id } = req.query as { id: string };
const { user, shareToken } = req.auth;
if (shareToken) {
return isValidToken(shareToken, { id });
}
if (user?.id) {
if (type === UmamiApi.AuthType.Website) {
const website = await cache.fetchWebsite(typeId ?? id);
if (website && website.userId === user.id) {
return true;
}
if (website.teamId) {
const teamUser = getTeamUser({ userId: user.id, teamId: website.teamId, isDeleted: false });
return teamUser;
}
return false;
} else if (type === UmamiApi.AuthType.User) {
const user = await getUser({ id });
return user && user.id === id;
} else if (type === UmamiApi.AuthType.Team) {
const teamUser = await getTeamUser({
userId: user.id,
teamId: typeId ?? id,
});
return teamUser;
} else if (type === UmamiApi.AuthType.TeamOwner) {
const teamUser = await getTeamUser({
userId: user.id,
teamId: typeId ?? id,
});
return (
teamUser &&
(teamUser.roleId === UmamiApi.Role.TeamOwner || teamUser.roleId === UmamiApi.Role.Admin)
);
}
}
return false;
}
export async function checkPermission(req: NextApiRequestAuth, type: UmamiApi.Permission) {
const {
user: { id: userId },
} = req.auth;
const userRole = await getPermissionsByUserId(userId, type);
return userRole.length > 0;
}
export async function canViewWebsite(userId: string, websiteId: string) { export async function canViewWebsite(userId: string, websiteId: string) {
const website = await cache.fetchWebsite(websiteId); const website = await cache.fetchWebsite(websiteId);
@ -128,7 +56,13 @@ export async function canViewWebsite(userId: string, websiteId: string) {
return userId === website.userId; return userId === website.userId;
} }
return false; if (website.teamId) {
const teamUser = await getTeamUser({ userId, teamId: website.teamId });
checkPermission(UmamiApi.Permission.websiteUpdate, teamUser.role as keyof UmamiApi.Roles);
}
return checkAdmin(userId);
} }
export async function canUpdateWebsite(userId: string, websiteId: string) { export async function canUpdateWebsite(userId: string, websiteId: string) {
@ -138,5 +72,100 @@ export async function canUpdateWebsite(userId: string, websiteId: string) {
return userId === website.userId; return userId === website.userId;
} }
return false; if (website.teamId) {
const teamUser = await getTeamUser({ userId, teamId: website.teamId });
checkPermission(UmamiApi.Permission.websiteUpdate, teamUser.role as keyof UmamiApi.Roles);
}
return checkAdmin(userId);
}
export async function canDeleteWebsite(userId: string, websiteId: string) {
const website = await cache.fetchWebsite(websiteId);
if (website.userId) {
return userId === website.userId;
}
if (website.teamId) {
const teamUser = await getTeamUser({ userId, teamId: website.teamId });
if (checkPermission(UmamiApi.Permission.websiteDelete, teamUser.role as keyof UmamiApi.Roles)) {
return true;
}
}
return checkAdmin(userId);
}
// To-do: Implement when payments are setup.
export async function canCreateTeam(userId: string) {
return !!userId;
}
// To-do: Implement when payments are setup.
export async function canViewTeam(userId: string, teamId) {
const teamUser = await getTeamUser({ userId, teamId });
return !!teamUser;
}
export async function canUpdateTeam(userId: string, teamId: string) {
const teamUser = await getTeamUser({ userId, teamId });
if (checkPermission(UmamiApi.Permission.teamUpdate, teamUser.role as keyof UmamiApi.Roles)) {
return true;
}
}
export async function canDeleteTeam(userId: string, teamId: string) {
const teamUser = await getTeamUser({ userId, teamId });
if (checkPermission(UmamiApi.Permission.teamDelete, teamUser.role as keyof UmamiApi.Roles)) {
return true;
}
}
export async function canCreateUser(userId: string) {
return checkAdmin(userId);
}
export async function canViewUser(userId: string, viewedUserId: string) {
if (userId === viewedUserId) {
return true;
}
return checkAdmin(userId);
}
export async function canViewUsers(userId: string) {
return checkAdmin(userId);
}
export async function canUpdateUser(userId: string, viewedUserId: string) {
if (userId === viewedUserId) {
return true;
}
return checkAdmin(userId);
}
export async function canUpdateUserRole(userId: string) {
return checkAdmin(userId);
}
export async function canDeleteUser(userId: string) {
return checkAdmin(userId);
}
export async function checkPermission(permission: UmamiApi.Permission, role: keyof UmamiApi.Roles) {
return UmamiApi.Roles[role].permissions.some(a => a === permission);
}
export async function checkAdmin(userId: string, userRoles?: UserRole[]) {
if (!userRoles) {
userRoles = await getUserRoles({ userId });
}
return userRoles.some(a => a.role === UmamiApi.Role.Admin);
} }

View File

@ -9,52 +9,56 @@ export namespace UmamiApi {
Website, Website,
User, User,
Team, Team,
TeamOwner,
} }
export enum Permission { export enum Permission {
Admin = 'Admin', all = 'all',
WebsiteCreate = 'website:create', websiteCreate = 'website:create',
WebsiteRead = 'website:read', websiteUpdate = 'website:update',
WebsiteUpdate = 'website:update', websiteDelete = 'website:delete',
WebsiteReset = 'website:reset', teamCreate = 'team:create',
WebsiteDelete = 'website:delete', teamUpdate = 'team:update',
TeamCreate = 'team:create', teamDelete = 'team:delete',
TeamUpdate = 'team:update',
TeamDelete = 'team:delete',
TeamAddUser = 'team:add-user',
TeamRemoveUser = 'team:remove-user',
} }
export enum Role { export enum Role {
Admin = 'Admin', Admin = 'admin',
Member = 'Member', User = 'user',
TeamOwner = 'Team Owner', TeamOwner = 'team-owner',
TeamMember = 'Team Member', TeamMember = 'team-member',
TeamGuest = 'Team Guest,', TeamGuest = 'team-guest',
} }
export const Roles = {
admin: { name: Role.Admin, permissions: [Permission.all] },
member: {
name: Role.User,
permissions: [
Permission.websiteCreate,
Permission.websiteUpdate,
Permission.websiteDelete,
Permission.teamCreate,
],
},
teamOwner: {
name: Role.TeamOwner,
permissions: [
Permission.teamUpdate,
Permission.teamDelete,
Permission.websiteCreate,
Permission.websiteUpdate,
Permission.websiteDelete,
],
},
teamMember: {
name: Role.TeamMember,
permissions: [Permission.websiteCreate, Permission.websiteUpdate, Permission.websiteDelete],
},
teamGuest: { name: Role.TeamGuest, permissions: [] },
};
export type Roles = typeof Roles;
} }
export const PERMISSIONS = {
all: 'all',
websiteCreate: 'website:create',
websiteUpdate: 'website:update',
websiteDelete: 'website:delete',
teamCreate: 'team:create',
teamUpdate: 'team:update',
teamDelete: 'team:delete',
};
export const ROLES = {
admin: { name: 'admin', permissions: [PERMISSIONS.all] },
teamOwner: { name: 'team-owner', permissions: [PERMISSIONS.teamUpdate, PERMISSIONS.teamDelete] },
teamMember: {
name: 'team-member',
permissions: [PERMISSIONS.websiteCreate, PERMISSIONS.websiteUpdate, PERMISSIONS.websiteDelete],
},
teamGuest: { name: 'team-guest' },
};
export const CURRENT_VERSION = process.env.currentVersion; export const CURRENT_VERSION = process.env.currentVersion;
export const AUTH_TOKEN = 'umami.auth'; export const AUTH_TOKEN = 'umami.auth';
export const LOCALE_CONFIG = 'umami.locale'; export const LOCALE_CONFIG = 'umami.locale';

View File

@ -1,7 +1,6 @@
import { Team } from '@prisma/client'; import { Team } from '@prisma/client';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
@ -21,12 +20,16 @@ export default async (
) => { ) => {
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const { id: teamId } = req.query; const { id: teamId } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { if (await canViewTeam(userId, teamId)) {
return unauthorized(res); return unauthorized(res);
} }
const user = await getTeam({ id: teamId }); const user = await getTeam({ id: teamId });
return ok(res, user); return ok(res, user);
@ -35,7 +38,7 @@ export default async (
if (req.method === 'POST') { if (req.method === 'POST') {
const { name } = req.body; const { name } = req.body;
if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { if (await canUpdateTeam(userId, teamId)) {
return unauthorized(res, 'You must be the owner of this team.'); return unauthorized(res, 'You must be the owner of this team.');
} }
@ -45,7 +48,7 @@ export default async (
} }
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { if (await canDeleteTeam(userId, teamId)) {
return unauthorized(res, 'You must be the owner of this team.'); return unauthorized(res, 'You must be the owner of this team.');
} }

View File

@ -1,6 +1,5 @@
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canUpdateTeam, canViewTeam } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
@ -22,10 +21,13 @@ export default async (
) => { ) => {
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const { id: teamId } = req.query; const { id: teamId } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { if (await canViewTeam(userId, teamId)) {
return unauthorized(res); return unauthorized(res);
} }
@ -35,7 +37,7 @@ export default async (
} }
if (req.method === 'POST') { if (req.method === 'POST') {
if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { if (await canUpdateTeam(userId, teamId)) {
return unauthorized(res, 'You must be the owner of this team.'); return unauthorized(res, 'You must be the owner of this team.');
} }
@ -54,7 +56,7 @@ export default async (
} }
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { if (await canUpdateTeam(userId, teamId)) {
return unauthorized(res, 'You must be the owner of this team.'); return unauthorized(res, 'You must be the owner of this team.');
} }
const { team_user_id } = req.body; const { team_user_id } = req.body;

View File

@ -1,6 +1,5 @@
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canViewTeam } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
@ -21,10 +20,13 @@ export default async (
) => { ) => {
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const { id: teamId } = req.query; const { id: teamId } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { if (await canViewTeam(userId, teamId)) {
return unauthorized(res); return unauthorized(res);
} }

View File

@ -1,9 +1,10 @@
import { Team } from '@prisma/client'; import { Team } from '@prisma/client';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { canCreateTeam } from 'lib/auth';
import { uuid } from 'lib/crypto'; import { uuid } from 'lib/crypto';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok } from 'next-basics'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createTeam, getTeam, getTeamsByUserId } from 'queries'; import { createTeam, getTeam, getTeamsByUserId } from 'queries';
export interface TeamsRequestBody { export interface TeamsRequestBody {
name: string; name: string;
@ -17,16 +18,20 @@ export default async (
await useAuth(req, res); await useAuth(req, res);
const { const {
user: { id }, user: { id: userId },
} = req.auth; } = req.auth;
if (req.method === 'GET') { if (req.method === 'GET') {
const teams = await getTeamsByUserId(id); const teams = await getTeamsByUserId(userId);
return ok(res, teams); return ok(res, teams);
} }
if (req.method === 'POST') { if (req.method === 'POST') {
if (await canCreateTeam(userId)) {
return unauthorized(res);
}
const { name } = req.body; const { name } = req.body;
const team = await getTeam({ name }); const team = await getTeam({ name });
@ -36,7 +41,7 @@ export default async (
} }
const created = await createTeam({ const created = await createTeam({
id: id || uuid(), id: uuid(),
name, name,
}); });

View File

@ -1,6 +1,5 @@
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { checkPermission } from 'lib/auth'; import { canDeleteUser, canUpdateUser, canViewUser, checkAdmin } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
@ -27,7 +26,7 @@ export default async (
const { id } = req.query; const { id } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (id !== userId) { if (await canViewUser(userId, id)) {
return unauthorized(res); return unauthorized(res);
} }
@ -37,12 +36,12 @@ export default async (
} }
if (req.method === 'POST') { if (req.method === 'POST') {
const { username, password } = req.body; if (await canUpdateUser(userId, id)) {
if (id !== userId) {
return unauthorized(res); return unauthorized(res);
} }
const { username, password } = req.body;
const user = await getUser({ id }); const user = await getUser({ id });
const data: any = {}; const data: any = {};
@ -52,7 +51,7 @@ export default async (
} }
// Only admin can change these fields // Only admin can change these fields
if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { if (username && (await checkAdmin(userId))) {
data.username = username; data.username = username;
} }
@ -71,12 +70,12 @@ export default async (
} }
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
if (id === userId) { if (canDeleteUser(userId)) {
return badRequest(res, 'You cannot delete your own user.'); return unauthorized(res);
} }
if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { if (id === userId) {
return unauthorized(res); return badRequest(res, 'You cannot delete your own user.');
} }
await deleteUser(id); await deleteUser(id);

View File

@ -1,6 +1,5 @@
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canUpdateUser } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { import {
@ -30,12 +29,15 @@ export default async (
const { current_password, new_password } = req.body; const { current_password, new_password } = req.body;
const { id } = req.query; const { id } = req.query;
const {
if (!(await allowQuery(req, UmamiApi.AuthType.User))) { user: { id: userId },
return unauthorized(res); } = req.auth;
}
if (req.method === 'POST') { if (req.method === 'POST') {
if (canUpdateUser(userId, id)) {
return unauthorized(res);
}
const user = await getUser({ id }); const user = await getUser({ id });
if (!checkPassword(current_password, user.password)) { if (!checkPassword(current_password, user.password)) {

View File

@ -1,20 +1,17 @@
import { UserRole } from '@prisma/client'; import { UserRole } from '@prisma/client';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { checkPermission } from 'lib/auth'; import { canUpdateUserRole } from 'lib/auth';
import { UmamiApi } from 'lib/constants'; import { UmamiApi } from 'lib/constants';
import { uuid } from 'lib/crypto';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
import { createUserRole, deleteUserRole, getUserRole, getUserRoles } from 'queries'; import { deleteUserRole, getUserRole, getUserRoles, updateUserRole } from 'queries';
export interface UserRoleRequestQuery { export interface UserRoleRequestQuery {
id: string; id: string;
} }
export interface UserRoleRequestBody { export interface UserRoleRequestBody {
roleId: string; role: UmamiApi.Role;
teamId?: string;
userRoleId?: string; userRoleId?: string;
} }
@ -29,7 +26,7 @@ export default async (
} = req.auth; } = req.auth;
const { id } = req.query; const { id } = req.query;
if (id !== userId || !(await checkPermission(req, UmamiApi.Permission.Admin))) { if (await canUpdateUserRole(userId)) {
return unauthorized(res); return unauthorized(res);
} }
@ -40,17 +37,17 @@ export default async (
} }
if (req.method === 'POST') { if (req.method === 'POST') {
const { roleId, teamId } = req.body; const { role } = req.body;
const userRole = getUserRole({ userId: id, roleId, teamId }); const userRole = await getUserRole({ userId: id });
if (userRole) { if (userRole && userRole.role === role) {
return badRequest(res, 'Role already exists for User.'); return badRequest(res, 'Role already exists for User.');
} else {
const updated = await updateUserRole({ role }, { id: userRole.id });
return ok(res, updated);
} }
const updated = await createUserRole({ id: uuid(), userId: id, roleId, teamId });
return ok(res, updated);
} }
if (req.method === 'DELETE') { if (req.method === 'DELETE') {

View File

@ -1,6 +1,5 @@
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { checkPermission } from 'lib/auth'; import { canCreateUser, canViewUsers } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { uuid } from 'lib/crypto'; import { uuid } from 'lib/crypto';
import { useAuth } from 'lib/middleware'; import { useAuth } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
@ -19,17 +18,25 @@ export default async (
) => { ) => {
await useAuth(req, res); await useAuth(req, res);
if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { const {
return unauthorized(res); user: { id: userId },
} } = req.auth;
if (req.method === 'GET') { if (req.method === 'GET') {
if (canViewUsers(userId)) {
return unauthorized(res);
}
const users = await getUsers(); const users = await getUsers();
return ok(res, users); return ok(res, users);
} }
if (req.method === 'POST') { if (req.method === 'POST') {
if (canCreateUser(userId)) {
return unauthorized(res);
}
const { username, password, id } = req.body; const { username, password, id } = req.body;
const user = await getUser({ username }); const user = await getUser({ username });

View File

@ -1,7 +1,6 @@
import { WebsiteActive } from 'interface/api/models'; import { WebsiteActive } from 'interface/api/models';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canViewWebsite } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
@ -18,13 +17,16 @@ export default async (
await useCors(req, res); await useCors(req, res);
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const { id: websiteId } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { if (await canViewWebsite(userId, websiteId)) {
return unauthorized(res); return unauthorized(res);
} }
const { id: websiteId } = req.query;
const result = await getActiveVisitors(websiteId); const result = await getActiveVisitors(websiteId);
return ok(res, result); return ok(res, result);

View File

@ -1,7 +1,6 @@
import { WebsiteMetric } from 'interface/api/models'; import { WebsiteMetric } from 'interface/api/models';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canViewWebsite } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
@ -26,13 +25,16 @@ export default async (
await useCors(req, res); await useCors(req, res);
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const { id: websiteId } = req.query;
if (req.method === 'POST') { if (req.method === 'POST') {
if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { if (canViewWebsite(userId, websiteId)) {
return unauthorized(res); return unauthorized(res);
} }
const { id: websiteId } = req.query;
const { start_at, end_at, event_name: eventName, columns, filters } = req.body; const { start_at, end_at, event_name: eventName, columns, filters } = req.body;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);

View File

@ -1,7 +1,6 @@
import { WebsiteMetric } from 'interface/api/models'; import { WebsiteMetric } from 'interface/api/models';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canViewWebsite } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
@ -27,13 +26,16 @@ export default async (
await useCors(req, res); await useCors(req, res);
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const { id: websiteId, start_at, end_at, unit, tz, url, event_name } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { if (canViewWebsite(userId, websiteId)) {
return unauthorized(res); return unauthorized(res);
} }
const { id: websiteId, start_at, end_at, unit, tz, url, event_name } = req.query;
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) { if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return badRequest(res); return badRequest(res);
} }

View File

@ -1,6 +1,6 @@
import { Website } from 'interface/api/models'; import { Website } from 'interface/api/models';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { canViewWebsite, canUpdateWebsite } from 'lib/auth'; import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics';
@ -23,10 +23,13 @@ export default async (
await useCors(req, res); await useCors(req, res);
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const { id: websiteId } = req.query; const { id: websiteId } = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth.user.id, websiteId))) { if (!(await canViewWebsite(userId, websiteId))) {
return unauthorized(res); return unauthorized(res);
} }
@ -36,7 +39,7 @@ export default async (
} }
if (req.method === 'POST') { if (req.method === 'POST') {
if (!(await canUpdateWebsite(req.auth.user.id, websiteId))) { if (!(await canUpdateWebsite(userId, websiteId))) {
return unauthorized(res); return unauthorized(res);
} }
@ -54,6 +57,10 @@ export default async (
} }
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
if (!(await canDeleteWebsite(userId, websiteId))) {
return unauthorized(res);
}
await deleteWebsite(websiteId); await deleteWebsite(websiteId);
return ok(res); return ok(res);

View File

@ -1,7 +1,7 @@
import { WebsiteMetric } from 'interface/api/models'; import { WebsiteMetric } from 'interface/api/models';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canViewWebsite } from 'lib/auth';
import { FILTER_IGNORED, UmamiApi } from 'lib/constants'; import { FILTER_IGNORED } from 'lib/constants';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
@ -56,24 +56,27 @@ export default async (
await useCors(req, res); await useCors(req, res);
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const {
id: websiteId,
type,
start_at,
end_at,
url,
referrer,
os,
browser,
device,
country,
} = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { if (!(await canViewWebsite(userId, websiteId))) {
return unauthorized(res); return unauthorized(res);
} }
const {
id: websiteId,
type,
start_at,
end_at,
url,
referrer,
os,
browser,
device,
country,
} = req.query;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);

View File

@ -1,7 +1,6 @@
import { WebsitePageviews } from 'interface/api/models'; import { WebsitePageviews } from 'interface/api/models';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canViewWebsite } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
@ -32,25 +31,28 @@ export default async (
await useCors(req, res); await useCors(req, res);
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const {
id: websiteId,
start_at,
end_at,
unit,
tz,
url,
referrer,
os,
browser,
device,
country,
} = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { if (!(await canViewWebsite(userId, websiteId))) {
return unauthorized(res); return unauthorized(res);
} }
const {
id: websiteId,
start_at,
end_at,
unit,
tz,
url,
referrer,
os,
browser,
device,
country,
} = req.query;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);

View File

@ -1,6 +1,5 @@
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canViewWebsite } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
@ -17,10 +16,13 @@ export default async (
await useCors(req, res); await useCors(req, res);
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const { id: websiteId } = req.query; const { id: websiteId } = req.query;
if (req.method === 'POST') { if (req.method === 'POST') {
if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { if (!(await canViewWebsite(userId, websiteId))) {
return unauthorized(res); return unauthorized(res);
} }

View File

@ -1,7 +1,6 @@
import { WebsiteStats } from 'interface/api/models'; import { WebsiteStats } from 'interface/api/models';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { allowQuery } from 'lib/auth'; import { canViewWebsite } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { methodNotAllowed, ok, unauthorized } from 'next-basics';
@ -27,23 +26,26 @@ export default async (
await useCors(req, res); await useCors(req, res);
await useAuth(req, res); await useAuth(req, res);
const {
user: { id: userId },
} = req.auth;
const {
id: websiteId,
start_at,
end_at,
url,
referrer,
os,
browser,
device,
country,
} = req.query;
if (req.method === 'GET') { if (req.method === 'GET') {
if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { if (!(await canViewWebsite(userId, websiteId))) {
return unauthorized(res); return unauthorized(res);
} }
const {
id: websiteId,
start_at,
end_at,
url,
referrer,
os,
browser,
device,
country,
} = req.query;
const startDate = new Date(+start_at); const startDate = new Date(+start_at);
const endDate = new Date(+end_at); const endDate = new Date(+end_at);

View File

@ -1,12 +1,11 @@
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiRequestQueryBody } from 'interface/api/nextApi';
import { checkAdmin } from 'lib/auth';
import { uuid } from 'lib/crypto'; import { uuid } from 'lib/crypto';
import { useAuth, useCors } from 'lib/middleware'; import { useAuth, useCors } from 'lib/middleware';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { methodNotAllowed, ok } from 'next-basics'; import { methodNotAllowed, ok } from 'next-basics';
import { createWebsite, getAllWebsites, getWebsitesByUserId } from 'queries'; import { createWebsite, getAllWebsites, getWebsitesByUserId } from 'queries';
import { checkPermission } from 'lib/auth';
import { UmamiApi } from 'lib/constants';
export interface WebsitesRequestQuery { export interface WebsitesRequestQuery {
include_all?: boolean; include_all?: boolean;
@ -33,7 +32,7 @@ export default async (
if (req.method === 'GET') { if (req.method === 'GET') {
const { include_all } = req.query; const { include_all } = req.query;
const isAdmin = await checkPermission(req, UmamiApi.Permission.Admin); const isAdmin = await checkAdmin(userId);
const websites = const websites =
isAdmin && include_all ? await getAllWebsites() : await getWebsitesByUserId(userId); isAdmin && include_all ? await getAllWebsites() : await getWebsitesByUserId(userId);
@ -44,7 +43,7 @@ export default async (
if (req.method === 'POST') { if (req.method === 'POST') {
const { name, domain, shareId, teamId } = req.body; const { name, domain, shareId, teamId } = req.body;
const data: Prisma.WebsiteCreateInput = { const data: Prisma.WebsiteUncheckedCreateInput = {
id: uuid(), id: uuid(),
name, name,
domain, domain,

View File

@ -1,83 +0,0 @@
import { Prisma, Permission } from '@prisma/client';
import prisma from 'lib/prisma';
export async function createPermission(data: Prisma.PermissionCreateInput): Promise<Permission> {
return prisma.client.permission.create({
data,
});
}
export async function getPermission(where: Prisma.PermissionWhereUniqueInput): Promise<Permission> {
return prisma.client.permission.findUnique({
where,
});
}
export async function getPermissions(where: Prisma.PermissionWhereInput): Promise<Permission[]> {
return prisma.client.permission.findMany({
where,
});
}
export async function getPermissionsByUserId(userId, name?: string): Promise<Permission[]> {
return prisma.client.permission.findMany({
where: {
...(name ? { name } : {}),
RolePermission: {
every: {
role: {
is: {
userRoles: {
every: {
userId,
},
},
},
},
},
},
},
});
}
export async function getPermissionsByTeamId(teamId, name?: string): Promise<Permission[]> {
return prisma.client.permission.findMany({
where: {
...(name ? { name } : {}),
RolePermission: {
every: {
role: {
is: {
TeamUser: {
every: {
teamId,
},
},
},
},
},
},
},
});
}
export async function updatePermission(
data: Prisma.PermissionUpdateInput,
where: Prisma.PermissionWhereUniqueInput,
): Promise<Permission> {
return prisma.client.permission.update({
data,
where,
});
}
export async function deletePermission(permissionId: string): Promise<Permission> {
return prisma.client.permission.update({
data: {
isDeleted: true,
},
where: {
id: permissionId,
},
});
}

View File

@ -1,57 +0,0 @@
import { Prisma, Role } from '@prisma/client';
import prisma from 'lib/prisma';
export async function createRole(data: {
id: string;
name: string;
description: string;
}): Promise<Role> {
return prisma.client.role.create({
data,
});
}
export async function getRole(where: Prisma.RoleWhereUniqueInput): Promise<Role> {
return prisma.client.role.findUnique({
where,
});
}
export async function getRoles(where: Prisma.RoleWhereInput): Promise<Role[]> {
return prisma.client.role.findMany({
where,
});
}
export async function getRolesByUserId(userId: string): Promise<Role[]> {
return prisma.client.role.findMany({
where: {
userRoles: {
every: {
userId,
},
},
},
});
}
export async function updateRole(
data: Prisma.RoleUpdateInput,
where: Prisma.RoleWhereUniqueInput,
): Promise<Role> {
return prisma.client.role.update({
data,
where,
});
}
export async function deleteRole(roleId: string): Promise<Role> {
return prisma.client.role.update({
data: {
isDeleted: true,
},
where: {
id: roleId,
},
});
}

View File

@ -5,14 +5,14 @@ import prisma from 'lib/prisma';
export async function createTeamUser( export async function createTeamUser(
userId: string, userId: string,
teamId: string, teamId: string,
roleId: string, role: string,
): Promise<TeamUser> { ): Promise<TeamUser> {
return prisma.client.teamUser.create({ return prisma.client.teamUser.create({
data: { data: {
id: uuid(), id: uuid(),
userId, userId,
teamId, teamId,
roleId, role,
}, },
}); });
} }

View File

@ -36,9 +36,7 @@ export async function getUser(
id: true, id: true,
username: true, username: true,
userRole: { userRole: {
include: { select: { role: true },
role: true,
},
}, },
password: includePassword, password: includePassword,
}, },

View File

@ -19,7 +19,10 @@ export async function createWebsite(
}); });
} }
export async function updateWebsite(websiteId, data: Prisma.WebsiteUpdateInput): Promise<Website> { export async function updateWebsite(
websiteId,
data: Prisma.WebsiteUpdateInput | Prisma.WebsiteUncheckedUpdateInput,
): Promise<Website> {
return prisma.client.website.update({ return prisma.client.website.update({
where: { where: {
id: websiteId, id: websiteId,
@ -97,7 +100,9 @@ export async function deleteWebsite(websiteId: string) {
}); });
} }
async function deleteWebsiteRelationalQuery(websiteId,): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { async function deleteWebsiteRelationalQuery(
websiteId,
): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> {
const { client, transaction } = prisma; const { client, transaction } = prisma;
return transaction([ return transaction([

View File

@ -1,5 +1,3 @@
export * from './admin/permission';
export * from './admin/role';
export * from './admin/team'; export * from './admin/team';
export * from './admin/teamUser'; export * from './admin/teamUser';
export * from './admin/user'; export * from './admin/user';