diff --git a/db/postgresql/migrations/01_init/migration.sql b/db/postgresql/migrations/01_init/migration.sql index 2ab8f8c3..620a8468 100644 --- a/db/postgresql/migrations/01_init/migration.sql +++ b/db/postgresql/migrations/01_init/migration.sql @@ -287,10 +287,10 @@ INSERT INTO "user" (user_id, username, password) VALUES ('41e2b680-648e-4b09-bcd -- Add Roles 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") (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") (gen_random_uuid(), 'Team Guest', 'View Websites.'); +INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Member', 'Create and maintain websites.'); +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") VALUES (gen_random_uuid(), 'Team Member', 'Create and maintain websites.'); +INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Team Guest', 'View Websites.'); -- Add Permissions INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'admin', 'System Admin'); 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 67b9b57c..abcb6c49 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,10 +1,10 @@ +import { UserRole } from '@prisma/client'; import debug from 'debug'; -import { NextApiRequestAuth } from 'interface/api/nextApi'; 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, getTeamUser, getUser } from 'queries'; +import { getTeamUser, getUserRoles } from 'queries'; 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) { try { if (typeof validation === 'object') { @@ -56,71 +49,6 @@ export function isValidToken(token, validation) { 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) { const website = await cache.fetchWebsite(websiteId); @@ -128,7 +56,13 @@ export async function canViewWebsite(userId: string, websiteId: string) { 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) { @@ -138,5 +72,100 @@ export async function canUpdateWebsite(userId: string, websiteId: string) { 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); } diff --git a/lib/constants.ts b/lib/constants.ts index 456fbf55..0da17d21 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -9,52 +9,56 @@ export namespace UmamiApi { Website, User, Team, - TeamOwner, } 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 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 AUTH_TOKEN = 'umami.auth'; export const LOCALE_CONFIG = 'umami.locale'; diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index e93621eb..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'; @@ -21,12 +20,16 @@ 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); } + const user = await getTeam({ id: teamId }); return ok(res, user); @@ -35,7 +38,7 @@ export default async ( if (req.method === 'POST') { 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.'); } @@ -45,7 +48,7 @@ export default async ( } 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.'); } 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 85b04148..9274d953 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -1,6 +1,6 @@ import { Website } from 'interface/api/models'; 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 { NextApiResponse } from 'next'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; @@ -23,10 +23,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 === 'GET') { - if (!(await canViewWebsite(req.auth.user.id, websiteId))) { + if (!(await canViewWebsite(userId, websiteId))) { return unauthorized(res); } @@ -36,7 +39,7 @@ export default async ( } if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth.user.id, websiteId))) { + if (!(await canUpdateWebsite(userId, websiteId))) { return unauthorized(res); } @@ -54,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 9667114a..00000000 --- a/queries/admin/permission.ts +++ /dev/null @@ -1,83 +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, name?: string): Promise { - return prisma.client.permission.findMany({ - where: { - ...(name ? { name } : {}), - RolePermission: { - every: { - role: { - is: { - userRoles: { - every: { - userId, - }, - }, - }, - }, - }, - }, - }, - }); -} - -export async function getPermissionsByTeamId(teamId, name?: string): Promise { - 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 { - return prisma.client.permission.update({ - data, - where, - }); -} - -export async function deletePermission(permissionId: string): Promise { - return prisma.client.permission.update({ - data: { - isDeleted: true, - }, - where: { - id: permissionId, - }, - }); -} diff --git a/queries/admin/role.ts b/queries/admin/role.ts deleted file mode 100644 index 2bf39930..00000000 --- a/queries/admin/role.ts +++ /dev/null @@ -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 { - 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, - }); -} - -export async function deleteRole(roleId: string): Promise { - return prisma.client.role.update({ - data: { - isDeleted: true, - }, - where: { - id: roleId, - }, - }); -} 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';