diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 7d478716..7ef3dd88 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -14,11 +14,9 @@ model User { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - groupRole GroupRole[] - groupUser GroupUser[] - userRole UserRole[] - teamUser TeamUser[] - Website Website[] + userRole UserRole[] + teamUser TeamUser[] + Website Website[] @@map("user") } @@ -78,96 +76,16 @@ model WebsiteEvent { @@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 { 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 createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - role Role @relation(fields: [roleId], references: [id]) user User @relation(fields: [userId], references: [id]) - @@unique([roleId, userId]) + @@unique([role, userId]) @@map("user_role") } @@ -187,13 +105,12 @@ model TeamUser { id String @id() @unique() @map("team_user_id") @db.Uuid teamId String @map("team_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) isDeleted Boolean @default(false) @map("is_deleted") team Team @relation(fields: [teamId], references: [id]) user User @relation(fields: [userId], references: [id]) - role Role @relation(fields: [roleId], references: [id]) @@map("team_user") } diff --git a/lib/auth.ts b/lib/auth.ts index 87cf016d..abcb6c49 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,9 +1,10 @@ +import { UserRole } from '@prisma/client'; import debug from 'debug'; import cache from 'lib/cache'; import { SHARE_TOKEN_HEADER, UmamiApi } from 'lib/constants'; import { secret } from 'lib/crypto'; import { parseSecureToken, parseToken } from 'next-basics'; -import { getPermissionsByUserId, getTeamPermissionsByUserId } from 'queries'; +import { getTeamUser, getUserRoles } from 'queries'; const log = debug('umami:auth'); @@ -48,45 +49,123 @@ export function isValidToken(token, validation) { return false; } -export async function allowQuery( - requestUserId: string, - type: UmamiApi.AuthType, - typeId?: string, - permission?: UmamiApi.Permission, -) { - if (type === UmamiApi.AuthType.Website) { - const website = await cache.fetchWebsite(typeId); +export async function canViewWebsite(userId: string, websiteId: string) { + const website = await cache.fetchWebsite(websiteId); - if (website && website.userId === requestUserId) { - return true; - } - - if (website.teamId) { - return checkTeamPermission(requestUserId, typeId, permission); - } - - return false; - } else if (type === UmamiApi.AuthType.User) { - if (requestUserId !== typeId) { - return checkUserPermission(requestUserId, permission || UmamiApi.Permission.Admin); - } - - return requestUserId === typeId; - } else if (type === UmamiApi.AuthType.Team) { - return checkTeamPermission(requestUserId, typeId, permission); + if (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 checkUserPermission(userId: string, type: UmamiApi.Permission) { - const userRole = await getPermissionsByUserId(userId, type); +export async function canUpdateWebsite(userId: string, websiteId: string) { + const website = await cache.fetchWebsite(websiteId); - return userRole.length > 0; + if (website.userId) { + return userId === website.userId; + } + + 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 checkTeamPermission(userId, teamId: string, type: UmamiApi.Permission) { - const userRole = await getTeamPermissionsByUserId(userId, teamId, type); +export async function canDeleteWebsite(userId: string, websiteId: string) { + const website = await cache.fetchWebsite(websiteId); - return userRole.length > 0; + 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); } diff --git a/lib/cache.ts b/lib/cache.ts index 0cf6e2c8..5c79def0 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,5 +1,5 @@ import { User, Website } from '@prisma/client'; -import redis, { DELETED } from 'lib/redis'; +import redis from 'lib/redis'; import { getSession, getUser, getWebsite } from '../queries'; async function fetchObject(key, query) { @@ -23,7 +23,7 @@ async function storeObject(key, data) { } async function deleteObject(key) { - return redis.set(key, DELETED); + return redis.set(key, redis.DELETED); } async function fetchWebsite(id): Promise { diff --git a/lib/constants.ts b/lib/constants.ts index 7537298a..0322f4e7 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -12,27 +12,54 @@ export namespace UmamiApi { } export enum Permission { - Admin = 'admin', - WebsiteCreate = 'website:create', - WebsiteRead = 'website:read', - WebsiteUpdate = 'website:update', - WebsiteReset = 'website:reset', - WebsiteDelete = 'website:delete', - TeamCreate = 'team:create', - TeamUpdate = 'team:update', - TeamDelete = 'team:delete', - TeamAddUser = 'team:add-user', - TeamRemoveUser = 'team:remove-user', + all = 'all', + websiteCreate = 'website:create', + websiteUpdate = 'website:update', + websiteDelete = 'website:delete', + teamCreate = 'team:create', + teamUpdate = 'team:update', + teamDelete = 'team:delete', } export enum Role { - Admin = 'Admin', - Member = 'Member', - TeamOwner = 'Team Owner', - TeamMember = 'Team Member', - TeamGuest = 'Team Guest,', + Admin = 'admin', + User = 'user', + TeamOwner = 'team-owner', + TeamMember = 'team-member', + 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 CURRENT_VERSION = process.env.currentVersion; export const AUTH_TOKEN = 'umami.auth'; export const LOCALE_CONFIG = 'umami.locale'; @@ -56,9 +83,6 @@ export const DEFAULT_WEBSITE_LIMIT = 10; export const REALTIME_RANGE = 30; export const REALTIME_INTERVAL = 3000; -export const TYPE_WEBSITE = 'website'; -export const TYPE_USER = 'user'; - export const THEME_COLORS = { light: { primary: '#2680eb', diff --git a/lib/redis.js b/lib/redis.js index 5ec4147d..fe236feb 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -3,16 +3,18 @@ import debug from 'debug'; const log = debug('umami:redis'); const REDIS = Symbol(); +const DELETED = 'DELETED'; let redis; -const enabled = Boolean(process.env.REDIS_URL); +const url = process.env.REDIS_URL; +const enabled = Boolean(url); async function getClient() { - if (!process.env.REDIS_URL) { + if (!enabled) { return null; } - const client = createClient({ url: process.env.REDIS_URL }); + const client = createClient({ url }); client.on('error', err => log(err)); await client.connect(); @@ -59,4 +61,4 @@ async function connect() { return redis; } -export default { enabled, client: redis, log, connect, get, set, del }; +export default { enabled, client: redis, log, connect, get, set, del, DELETED }; diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index ce330aa5..65a344c5 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -1,7 +1,6 @@ import { Team } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -27,7 +26,7 @@ export default async ( const { id: teamId } = req.query; if (req.method === 'GET') { - if (!(await allowQuery(userId, UmamiApi.AuthType.Team, teamId))) { + if (await canViewTeam(userId, teamId)) { return unauthorized(res); } @@ -39,9 +38,7 @@ export default async ( if (req.method === 'POST') { const { name } = req.body; - if ( - !(await allowQuery(userId, UmamiApi.AuthType.Team, teamId, UmamiApi.Permission.TeamUpdate)) - ) { + if (await canUpdateTeam(userId, teamId)) { return unauthorized(res, 'You must be the owner of this team.'); } @@ -51,9 +48,7 @@ export default async ( } if (req.method === 'DELETE') { - if ( - !(await allowQuery(userId, UmamiApi.AuthType.Team, teamId, UmamiApi.Permission.TeamDelete)) - ) { + if (await canDeleteTeam(userId, teamId)) { return unauthorized(res, 'You must be the owner of this team.'); } diff --git a/pages/api/teams/[id]/user.ts b/pages/api/teams/[id]/user.ts index 76e60217..8b6aaf30 100644 --- a/pages/api/teams/[id]/user.ts +++ b/pages/api/teams/[id]/user.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canUpdateTeam, canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -22,10 +21,13 @@ export default async ( ) => { await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; const { id: teamId } = req.query; if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { + if (await canViewTeam(userId, teamId)) { return unauthorized(res); } @@ -35,7 +37,7 @@ export default async ( } 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.'); } @@ -54,7 +56,7 @@ export default async ( } 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.'); } const { team_user_id } = req.body; diff --git a/pages/api/teams/[id]/website.ts b/pages/api/teams/[id]/website.ts index 364fc5da..3d959e70 100644 --- a/pages/api/teams/[id]/website.ts +++ b/pages/api/teams/[id]/website.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -21,10 +20,13 @@ export default async ( ) => { await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; const { id: teamId } = req.query; if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { + if (await canViewTeam(userId, teamId)) { return unauthorized(res); } diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index 5e8f6f4f..2831d07d 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -1,9 +1,10 @@ import { Team } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { canCreateTeam } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; 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'; export interface TeamsRequestBody { name: string; @@ -17,16 +18,20 @@ export default async ( await useAuth(req, res); const { - user: { id }, + user: { id: userId }, } = req.auth; if (req.method === 'GET') { - const teams = await getTeamsByUserId(id); + const teams = await getTeamsByUserId(userId); return ok(res, teams); } if (req.method === 'POST') { + if (await canCreateTeam(userId)) { + return unauthorized(res); + } + const { name } = req.body; const team = await getTeam({ name }); @@ -36,7 +41,7 @@ export default async ( } const created = await createTeam({ - id: id || uuid(), + id: uuid(), name, }); diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index 8eef949b..d5215f23 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { checkPermission } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canDeleteUser, canUpdateUser, canViewUser, checkAdmin } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -27,7 +26,7 @@ export default async ( const { id } = req.query; if (req.method === 'GET') { - if (id !== userId) { + if (await canViewUser(userId, id)) { return unauthorized(res); } @@ -37,12 +36,12 @@ export default async ( } if (req.method === 'POST') { - const { username, password } = req.body; - - if (id !== userId) { + if (await canUpdateUser(userId, id)) { return unauthorized(res); } + const { username, password } = req.body; + const user = await getUser({ id }); const data: any = {}; @@ -52,7 +51,7 @@ export default async ( } // Only admin can change these fields - if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { + if (username && (await checkAdmin(userId))) { data.username = username; } @@ -71,12 +70,12 @@ export default async ( } if (req.method === 'DELETE') { - if (id === userId) { - return badRequest(res, 'You cannot delete your own user.'); + if (canDeleteUser(userId)) { + return unauthorized(res); } - if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { - return unauthorized(res); + if (id === userId) { + return badRequest(res, 'You cannot delete your own user.'); } await deleteUser(id); diff --git a/pages/api/users/[id]/password.ts b/pages/api/users/[id]/password.ts index e0024eda..5727cfbd 100644 --- a/pages/api/users/[id]/password.ts +++ b/pages/api/users/[id]/password.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canUpdateUser } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { @@ -30,12 +29,15 @@ export default async ( const { current_password, new_password } = req.body; const { id } = req.query; - - if (!(await allowQuery(req, UmamiApi.AuthType.User))) { - return unauthorized(res); - } + const { + user: { id: userId }, + } = req.auth; if (req.method === 'POST') { + if (canUpdateUser(userId, id)) { + return unauthorized(res); + } + const user = await getUser({ id }); if (!checkPassword(current_password, user.password)) { diff --git a/pages/api/users/[id]/role.ts b/pages/api/users/[id]/role.ts index fe3dcbd4..fd4d33a6 100644 --- a/pages/api/users/[id]/role.ts +++ b/pages/api/users/[id]/role.ts @@ -1,20 +1,17 @@ import { UserRole } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { checkPermission } from 'lib/auth'; +import { canUpdateUserRole } from 'lib/auth'; import { UmamiApi } from 'lib/constants'; -import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; 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 { id: string; } - export interface UserRoleRequestBody { - roleId: string; - teamId?: string; + role: UmamiApi.Role; userRoleId?: string; } @@ -29,7 +26,7 @@ export default async ( } = req.auth; const { id } = req.query; - if (id !== userId || !(await checkPermission(req, UmamiApi.Permission.Admin))) { + if (await canUpdateUserRole(userId)) { return unauthorized(res); } @@ -40,17 +37,17 @@ export default async ( } 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.'); + } 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') { diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 5f15c44a..b64c51ac 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { checkPermission } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canCreateUser, canViewUsers } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; @@ -19,17 +18,25 @@ export default async ( ) => { await useAuth(req, res); - if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { - return unauthorized(res); - } + const { + user: { id: userId }, + } = req.auth; if (req.method === 'GET') { + if (canViewUsers(userId)) { + return unauthorized(res); + } + const users = await getUsers(); return ok(res, users); } if (req.method === 'POST') { + if (canCreateUser(userId)) { + return unauthorized(res); + } + const { username, password, id } = req.body; const user = await getUser({ username }); diff --git a/pages/api/websites/[id]/active.ts b/pages/api/websites/[id]/active.ts index 3323ad7b..66b02d1d 100644 --- a/pages/api/websites/[id]/active.ts +++ b/pages/api/websites/[id]/active.ts @@ -1,7 +1,6 @@ import { WebsiteActive } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -18,13 +17,16 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; + const { id: websiteId } = req.query; + if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (await canViewWebsite(userId, websiteId)) { return unauthorized(res); } - const { id: websiteId } = req.query; - const result = await getActiveVisitors(websiteId); return ok(res, result); diff --git a/pages/api/websites/[id]/eventdata.ts b/pages/api/websites/[id]/eventdata.ts index 120f61cc..e9df136f 100644 --- a/pages/api/websites/[id]/eventdata.ts +++ b/pages/api/websites/[id]/eventdata.ts @@ -1,7 +1,6 @@ import { WebsiteMetric } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -26,13 +25,16 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; + const { id: websiteId } = req.query; + if (req.method === 'POST') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (canViewWebsite(userId, websiteId)) { return unauthorized(res); } - const { id: websiteId } = req.query; - const { start_at, end_at, event_name: eventName, columns, filters } = req.body; const startDate = new Date(+start_at); diff --git a/pages/api/websites/[id]/events.ts b/pages/api/websites/[id]/events.ts index 832cb727..c2c6a8cc 100644 --- a/pages/api/websites/[id]/events.ts +++ b/pages/api/websites/[id]/events.ts @@ -1,7 +1,6 @@ import { WebsiteMetric } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import moment from 'moment-timezone'; import { NextApiResponse } from 'next'; @@ -27,13 +26,16 @@ export default async ( await useCors(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 (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (canViewWebsite(userId, websiteId)) { 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)) { return badRequest(res); } diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 356feb0a..9274d953 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -1,7 +1,6 @@ import { Website } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; @@ -24,19 +23,26 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; const { id: websiteId } = req.query; - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { - return unauthorized(res); - } - if (req.method === 'GET') { + if (!(await canViewWebsite(userId, websiteId))) { + return unauthorized(res); + } + const website = await getWebsite({ id: websiteId }); return ok(res, website); } if (req.method === 'POST') { + if (!(await canUpdateWebsite(userId, websiteId))) { + return unauthorized(res); + } + const { name, domain, shareId } = req.body; try { @@ -51,6 +57,10 @@ export default async ( } if (req.method === 'DELETE') { + if (!(await canDeleteWebsite(userId, websiteId))) { + return unauthorized(res); + } + await deleteWebsite(websiteId); return ok(res); diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index d3cdbf07..e84ff1fe 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -1,7 +1,7 @@ import { WebsiteMetric } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { FILTER_IGNORED, UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; +import { FILTER_IGNORED } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -56,24 +56,27 @@ export default async ( await useCors(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 (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (!(await canViewWebsite(userId, websiteId))) { 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 endDate = new Date(+end_at); diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index 208a052a..37ad891f 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -1,7 +1,6 @@ import { WebsitePageviews } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import moment from 'moment-timezone'; import { NextApiResponse } from 'next'; @@ -32,25 +31,28 @@ export default async ( await useCors(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 (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (!(await canViewWebsite(userId, websiteId))) { 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 endDate = new Date(+end_at); diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index a5473ee6..d6c049df 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -17,10 +16,13 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; const { id: websiteId } = req.query; if (req.method === 'POST') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (!(await canViewWebsite(userId, websiteId))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index 2122a2da..f326db30 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -1,7 +1,6 @@ import { WebsiteStats } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -27,23 +26,26 @@ export default async ( await useCors(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 (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (!(await canViewWebsite(userId, websiteId))) { 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 endDate = new Date(+end_at); diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index 85da6c10..8e4408bc 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,12 +1,11 @@ import { Prisma } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { checkAdmin } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok } from 'next-basics'; import { createWebsite, getAllWebsites, getWebsitesByUserId } from 'queries'; -import { checkPermission } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; export interface WebsitesRequestQuery { include_all?: boolean; @@ -33,7 +32,7 @@ export default async ( if (req.method === 'GET') { const { include_all } = req.query; - const isAdmin = await checkPermission(req, UmamiApi.Permission.Admin); + const isAdmin = await checkAdmin(userId); const websites = isAdmin && include_all ? await getAllWebsites() : await getWebsitesByUserId(userId); @@ -44,7 +43,7 @@ export default async ( if (req.method === 'POST') { const { name, domain, shareId, teamId } = req.body; - const data: Prisma.WebsiteCreateInput = { + const data: Prisma.WebsiteUncheckedCreateInput = { id: uuid(), name, domain, diff --git a/queries/admin/permission.ts b/queries/admin/permission.ts deleted file mode 100644 index bd851369..00000000 --- a/queries/admin/permission.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Prisma, Permission } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function createPermission(data: Prisma.PermissionCreateInput): Promise { - return prisma.client.permission.create({ - data, - }); -} - -export async function getPermission(where: Prisma.PermissionWhereUniqueInput): Promise { - return prisma.client.permission.findUnique({ - where, - }); -} - -export async function getPermissions(where: Prisma.PermissionWhereInput): Promise { - return prisma.client.permission.findMany({ - where, - }); -} - -export async function getPermissionsByUserId(userId: string, name?: string): Promise { - return prisma.client.permission.findMany({ - where: { - ...(name ? { name } : {}), - RolePermission: { - every: { - role: { - is: { - userRoles: { - every: { - userId, - }, - }, - }, - }, - }, - }, - }, - }); -} - -export async function getTeamPermissionsByUserId( - userId: string, - teamId: string, - name?: string, -): Promise { - return prisma.client.permission.findMany({ - where: { - ...(name ? { name } : {}), - RolePermission: { - every: { - role: { - is: { - TeamUser: { - every: { - userId, - teamId, - }, - }, - }, - }, - }, - }, - }, - }); -} - -export async function updatePermission( - data: Prisma.PermissionUpdateInput, - where: Prisma.PermissionWhereUniqueInput, -): Promise { - return prisma.client.permission.update({ - data, - where, - }); -} diff --git a/queries/admin/role.ts b/queries/admin/role.ts deleted file mode 100644 index 08da6237..00000000 --- a/queries/admin/role.ts +++ /dev/null @@ -1,46 +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 { - return prisma.client.role.create({ - data, - }); -} - -export async function getRole(where: Prisma.RoleWhereUniqueInput): Promise { - return prisma.client.role.findUnique({ - where, - }); -} - -export async function getRoles(where: Prisma.RoleWhereInput): Promise { - return prisma.client.role.findMany({ - where, - }); -} - -export async function getRolesByUserId(userId: string): Promise { - return prisma.client.role.findMany({ - where: { - userRoles: { - every: { - userId, - }, - }, - }, - }); -} - -export async function updateRole( - data: Prisma.RoleUpdateInput, - where: Prisma.RoleWhereUniqueInput, -): Promise { - return prisma.client.role.update({ - data, - where, - }); -} diff --git a/queries/admin/teamUser.ts b/queries/admin/teamUser.ts index a0160560..e88fa5e1 100644 --- a/queries/admin/teamUser.ts +++ b/queries/admin/teamUser.ts @@ -5,14 +5,14 @@ import prisma from 'lib/prisma'; export async function createTeamUser( userId: string, teamId: string, - roleId: string, + role: string, ): Promise { return prisma.client.teamUser.create({ data: { id: uuid(), userId, teamId, - roleId, + role, }, }); } diff --git a/queries/admin/user.ts b/queries/admin/user.ts index 7122323b..81727b16 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -36,9 +36,7 @@ export async function getUser( id: true, username: true, userRole: { - include: { - role: true, - }, + select: { role: true }, }, password: includePassword, }, diff --git a/queries/admin/website.ts b/queries/admin/website.ts index ad742823..c2ed683b 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -19,7 +19,10 @@ export async function createWebsite( }); } -export async function updateWebsite(websiteId, data: Prisma.WebsiteUpdateInput): Promise { +export async function updateWebsite( + websiteId, + data: Prisma.WebsiteUpdateInput | Prisma.WebsiteUncheckedUpdateInput, +): Promise { return prisma.client.website.update({ where: { 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; return transaction([ diff --git a/queries/index.js b/queries/index.js index 1a029edb..f1f03375 100644 --- a/queries/index.js +++ b/queries/index.js @@ -1,5 +1,3 @@ -export * from './admin/permission'; -export * from './admin/role'; export * from './admin/team'; export * from './admin/teamUser'; export * from './admin/user';