feat(frontend): user permissions

This commit is contained in:
FrenchGithubUser
2025-12-14 14:35:52 +01:00
parent 0ef99532c9
commit b8f522116f
19 changed files with 97 additions and 112 deletions

View File

@@ -11,6 +11,7 @@ import { ref } from 'vue'
import { Button } from 'primevue'
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import type { UserPermission } from '@/services/api-schema'
const userStore = useUserStore()
@@ -29,7 +30,9 @@ const menuItems = ref([
])
onMounted(() => {
if (userStore.class === 'staff') {
// if the user can do one of those actions, they can access the staff dashboard
const permissionsToSeeStaffDashboard: UserPermission[] = ['create_css_sheet', 'edit_css_sheet', 'get_user_application', 'read_staff_pm']
if (permissionsToSeeStaffDashboard.some((x: UserPermission) => userStore.permissions.includes(x))) {
menuItems.value.push({ label: 'Staff Dashboard', route: '/staff-dashboard' })
}
})

View File

@@ -4,7 +4,7 @@
<div class="actions">
<i
class="pi pi-pen-to-square"
v-if="userStore.class === 'staff' || artist.created_by_id === userStore.id"
v-if="userStore.permissions.includes('edit_artist') || artist.created_by_id === userStore.id"
v-tooltip.top="t('artist.edit')"
@click="editArtist"
/>

View File

@@ -3,7 +3,7 @@
<div class="actions">
<i
class="pi pi-pen-to-square"
v-if="(userStore.id === comment.created_by.id && 'locked' in comment && comment.locked === false) || userStore.class === 'staff'"
v-if="(userStore.id === comment.created_by.id && 'locked' in comment && comment.locked === false) || hasEditPermission"
@click="editCommentDialogVisible = true"
/>
<RouterLink
@@ -55,6 +55,7 @@ const props = defineProps<{
comment: TitleGroupCommentHierarchy | ForumPostHierarchy | ConversationMessageHierarchy
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
editCommentMethod?: Function
hasEditPermission: boolean
}>()
const emit = defineEmits<{

View File

@@ -5,6 +5,7 @@
:key="message.id"
:comment="message"
:class="`message ${userStore.id === message.created_by.id ? 'sent' : 'received'}`"
:hasEditPermission="false"
/>
</div>
</template>

View File

@@ -10,7 +10,7 @@
<label for="subcategory">{{ t('forum.subcategory') }}</label>
</FloatLabel>
<div v-if="userStore.class === 'staff'" class="staff-options">
<div v-if="userStore.permissions.includes('edit_forum_thread')" class="staff-options">
<div class="checkbox-row">
<Checkbox v-model="editedThread.locked" binary inputId="locked" />
<label for="locked">{{ t('general.locked') }}</label>

View File

@@ -3,12 +3,16 @@
<div class="top">
<div class="title">{{ forumCategory.name }}</div>
<div class="actions">
<RouterLink :to="`/forum/category/${forumCategory.id}/edit`" v-if="userStore.class === 'staff'" v-tooltip.top="t('forum.edit_category')">
<RouterLink
:to="`/forum/category/${forumCategory.id}/edit`"
v-if="userStore.permissions.includes('edit_forum_category')"
v-tooltip.top="t('forum.edit_category')"
>
<i class="pi pi-pen-to-square" />
</RouterLink>
<RouterLink
:to="{ path: '/forum/sub-category/new', query: { categoryId: forumCategory.id, categoryName: forumCategory.name } }"
v-if="userStore.class === 'staff'"
v-if="userStore.permissions.includes('create_forum_sub_category')"
v-tooltip.top="t('forum.create_sub_category')"
>
<i class="pi pi-plus" />

View File

@@ -6,6 +6,7 @@
:comment="comment"
@commentEdited="commentEdited($event, comment.id)"
:editCommentMethod="(post: EditedTitleGroupComment) => editTitleGroupComment({ EditedTitleGroupComment: post, id: comment.id })"
:hasEditPermission="useUserStore().permissions.includes('edit_title_group_comment')"
/>
</div>
<Form v-slot="$form" :initialValues="new_comment" :resolver @submit="onFormSubmit" validateOnSubmit :validateOnValueUpdate="false">

View File

@@ -49,11 +49,11 @@
<i
v-tooltip.top="t('general.delete')"
class="action pi pi-trash"
v-if="showActionBtns && (user.id === slotProps.data.created_by_id || user.class === 'staff')"
v-if="showActionBtns && (user.id === slotProps.data.created_by_id || user.permissions.includes('delete_torrent'))"
@click="deleteTorrent(slotProps.data.id)"
/>
<i
v-if="showActionBtns && (user.id === slotProps.data.created_by_id || user.class === 'staff')"
v-if="showActionBtns && (user.id === slotProps.data.created_by_id || user.permissions.includes('edit_torrent'))"
v-tooltip.top="t('general.edit')"
@click="editTorrent(slotProps.data)"
class="action pi pi-pen-to-square"

View File

@@ -8,7 +8,7 @@
{{ t('user.last_seen') }}:
<span v-tooltip.top="formatDate(user.last_seen)">{{ timeAgo(user.last_seen) }}</span>
<br />
{{ t('user.class') }}: {{ user.class }}
{{ t('user.class') }}: {{ user.class_name }}
<br />
{{ t('user.bonus_points') }}: {{ user.bonus_points }}
<br />

View File

@@ -1049,7 +1049,7 @@ export interface PublicUser {
'average_seeding_time': number;
'banned': boolean;
'bonus_points': number;
'class': UserClass;
'class_name': string;
'collages_started': number;
'created_at': string;
'description': string;
@@ -1076,8 +1076,6 @@ export interface PublicUser {
'username': string;
'warned': boolean;
}
export interface RefreshToken {
'refresh_token': string;
}
@@ -1495,11 +1493,6 @@ export interface TorrentHierarchyLite {
}
export interface TorrentMinimal {
'created_at': string;
'id': number;
'info_hash'?: string | null;
}
export interface TorrentReport {
'description': string;
'id': number;
@@ -1669,7 +1662,8 @@ export interface User {
'average_seeding_time': number;
'banned': boolean;
'bonus_points': number;
'class': UserClass;
'class_locked': boolean;
'class_name': string;
'collages_started': number;
'created_at': string;
'css_sheet_name': string;
@@ -1686,6 +1680,7 @@ export interface User {
'leeching': number;
'passkey': string;
'password_hash': string;
'permissions': Array<UserPermission>;
'ratio': number;
'real_downloaded': number;
'real_uploaded': number;
@@ -1703,8 +1698,6 @@ export interface User {
'username': string;
'warned': boolean;
}
export interface UserApplication {
'applied_from_ip': string;
'body': string;
@@ -1727,16 +1720,6 @@ export const UserApplicationStatus = {
export type UserApplicationStatus = typeof UserApplicationStatus[keyof typeof UserApplicationStatus];
export const UserClass = {
Newbie: 'newbie',
Staff: 'staff',
Tracker: 'tracker'
} as const;
export type UserClass = typeof UserClass[keyof typeof UserClass];
export interface UserCreatedAffiliatedArtist {
'artist_id': number;
'nickname'?: string | null;
@@ -1924,6 +1907,50 @@ export interface UserLiteAvatar {
'username': string;
'warned': boolean;
}
export const UserPermission = {
UploadTorrent: 'upload_torrent',
DownloadTorrent: 'download_torrent',
CreateTorrentRequest: 'create_torrent_request',
ImmuneActivityPruning: 'immune_activity_pruning',
EditTitleGroup: 'edit_title_group',
EditTitleGroupComment: 'edit_title_group_comment',
EditEditionGroup: 'edit_edition_group',
EditTorrent: 'edit_torrent',
EditArtist: 'edit_artist',
EditCollage: 'edit_collage',
EditSeries: 'edit_series',
EditTorrentRequest: 'edit_torrent_request',
EditForumPost: 'edit_forum_post',
EditForumThread: 'edit_forum_thread',
EditForumSubCategory: 'edit_forum_sub_category',
EditForumCategory: 'edit_forum_category',
CreateForumCategory: 'create_forum_category',
CreateForumSubCategory: 'create_forum_sub_category',
CreateForumThread: 'create_forum_thread',
CreateForumPost: 'create_forum_post',
SendPm: 'send_pm',
CreateCssSheet: 'create_css_sheet',
EditCssSheet: 'edit_css_sheet',
SetDefaultCssSheet: 'set_default_css_sheet',
ReadStaffPm: 'read_staff_pm',
ReplyStaffPm: 'reply_staff_pm',
ResolveStaffPm: 'resolve_staff_pm',
UnresolveStaffPm: 'unresolve_staff_pm',
DeleteTitleGroupTag: 'delete_title_group_tag',
EditTitleGroupTag: 'edit_title_group_tag',
DeleteTorrent: 'delete_torrent',
GetUserApplication: 'get_user_application',
UpdateUserApplication: 'update_user_application',
WarnUser: 'warn_user',
EditUser: 'edit_user',
CreateWikiArticle: 'create_wiki_article',
EditWikiArticle: 'edit_wiki_article'
} as const;
export type UserPermission = typeof UserPermission[keyof typeof UserPermission];
export interface UserSettings {
'css_sheet_name': string;
}
@@ -9172,39 +9199,6 @@ export const TorrentApiAxiosParamCreator = function (configuration?: Configurati
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRegisteredTorrents: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/torrents/registered`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication http required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} period
@@ -9370,17 +9364,6 @@ export const TorrentApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['TorrentApi.editTorrent']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getRegisteredTorrents(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TorrentMinimal>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getRegisteredTorrents(options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['TorrentApi.getRegisteredTorrents']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {string} period
@@ -9479,14 +9462,6 @@ export const TorrentApiFactory = function (configuration?: Configuration, basePa
editTorrent(editedTorrent: EditedTorrent, options?: RawAxiosRequestConfig): AxiosPromise<Torrent> {
return localVarFp.editTorrent(editedTorrent, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRegisteredTorrents(options?: RawAxiosRequestConfig): AxiosPromise<Array<TorrentMinimal>> {
return localVarFp.getRegisteredTorrents(options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} period
@@ -9582,15 +9557,6 @@ export class TorrentApi extends BaseAPI {
return TorrentApiFp(this.configuration).editTorrent(editedTorrent, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public getRegisteredTorrents(options?: RawAxiosRequestConfig) {
return TorrentApiFp(this.configuration).getRegisteredTorrents(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} period
@@ -9691,12 +9657,6 @@ export const editTorrent = async (editedTorrent: EditedTorrent, options?: RawAxi
return response.data;
};
export const getRegisteredTorrents = async (options?: RawAxiosRequestConfig): Promise<Array<TorrentMinimal>> => {
const response = await torrentApi.getRegisteredTorrents(options);
return response.data;
};
export interface GetTopTorrentRequest {
/** */
'period': string;
@@ -10365,7 +10325,7 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
warnUser: async (userCreatedUserWarning: UserCreatedUserWarning, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'userCreatedUserWarning' is not null or undefined
assertParamExists('warnUser', 'userCreatedUserWarning', userCreatedUserWarning)
const localVarPath = `/api/users/warnings`;
const localVarPath = `/api/users/warn`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;

View File

@@ -6,7 +6,9 @@ const initialState: User = {
avatar: null,
average_seeding_time: 0,
bonus_points: 0,
class: 'newbie',
class_name: 'newbie',
class_locked: false,
permissions: [],
collages_started: 0,
created_at: '',
description: '',

View File

@@ -64,10 +64,20 @@
</span>
</template>
</Column>
<Column :header="t('general.action', 2)" class="actions" v-if="userStore.class === 'staff'">
<Column :header="t('general.action', 2)" class="actions">
<template #body="slotProps">
<i class="pi pi-pen-to-square cursor-pointer" v-tooltip.top="t('general.edit')" @click="editTag(slotProps.data)" />
<i class="pi pi-trash cursor-pointer" v-tooltip.top="t('general.delete')" @click="deleteTag(slotProps.data)" />
<i
class="pi pi-pen-to-square cursor-pointer"
v-if="userStore.permissions.includes('edit_title_group_tag')"
v-tooltip.top="t('general.edit')"
@click="editTag(slotProps.data)"
/>
<i
class="pi pi-trash cursor-pointer"
v-if="userStore.permissions.includes('delete_title_group_tag')"
v-tooltip.top="t('general.delete')"
@click="deleteTag(slotProps.data)"
/>
</template>
</Column>
</DataTable>

View File

@@ -20,7 +20,7 @@
</div>
<div>
<i
v-if="titleGroupAndAssociatedData.title_group.created_by_id === userStore.id || userStore.class === 'staff'"
v-if="titleGroupAndAssociatedData.title_group.created_by_id === userStore.id || userStore.permissions.includes('edit_title_group')"
v-tooltip.top="t('general.edit')"
class="pi pi-pen-to-square"
@click="editTitleGroupDialogVisible = true"

View File

@@ -11,7 +11,7 @@
<RouterLink :to="`/conversation/new?receiverId=${user.id}&username=${user.username}`" class="no-color" v-if="userStore.id !== user.id">
<i v-tooltip.top="t('user.message_user', [user.username])" class="pi pi-envelope" />
</RouterLink>
<template v-if="userStore.class === 'staff' && userStore.id !== user.id">
<template v-if="userStore.permissions.includes('warn_user') && userStore.id !== user.id">
<i v-tooltip.top="t('user.warn')" class="cursor-pointer pi pi-exclamation-triangle" @click="warnUserDialogVisible = true" />
</template>
<template v-if="userStore.id === user.id">

View File

@@ -13,7 +13,7 @@
<RouterLink to="/forum/search">
<i class="pi pi-search" v-tooltip.top="t('forum.search')" />
</RouterLink>
<RouterLink to="/forum/category/new" v-if="userStore.class === 'staff'">
<RouterLink to="/forum/category/new" v-if="userStore.permissions.includes('create_forum_category')">
<i class="pi pi-plus" v-tooltip.top="t('forum.create_category')" />
</RouterLink>
</div>

View File

@@ -6,7 +6,7 @@
<RouterLink to="">{{ forumSubCategory.name }}</RouterLink>
</div>
<div class="actions">
<RouterLink :to="`/forum/sub-category/${route.params.id}/edit`" v-if="forumSubCategory && userStore.class === 'staff'">
<RouterLink :to="`/forum/sub-category/${route.params.id}/edit`" v-if="forumSubCategory && userStore.permissions.includes('edit_forum_sub_category')">
<i v-tooltip.top="t('forum.edit_subcategory')" class="pi pi-pen-to-square cursor-pointer" />
</RouterLink>
<RouterLink :to="`/forum/thread/new?subCategoryId=${route.params.id}`">

View File

@@ -8,7 +8,7 @@
</div>
<div class="actions">
<i
v-if="userStore.class === 'staff' || forumThread.created_by_id === userStore.id"
v-if="userStore.permissions.includes('edit_forum_thread') || forumThread.created_by_id === userStore.id"
class="pi pi-pen-to-square"
v-tooltip.top="t('forum.edit_thread')"
@click="editThreadDialogVisible = true"
@@ -36,6 +36,7 @@
:comment="post"
:editCommentMethod="editForumPostMethod"
@commentEdited="postEdited($event as EditedForumPost)"
:hasEditPermission="userStore.permissions.includes('edit_forum_post')"
/>
</PaginatedResults>
<Form v-slot="$form" :initialValues="newPost" :resolver @submit="onFormSubmit" validateOnSubmit :validateOnValueUpdate="false">

View File

@@ -7,13 +7,13 @@
<Tab value="2">{{ t('css_sheet.css_sheet', 2) }}</Tab>
</TabList>
<TabPanels>
<TabPanel value="0">
<TabPanel value="0" v-if="userStore.permissions.includes('get_user_application')">
<UserApplications />
</TabPanel>
<TabPanel value="1">
<TabPanel value="1" v-if="userStore.permissions.includes('read_staff_pm')">
<StaffPmsTable />
</TabPanel>
<TabPanel value="2">
<TabPanel value="2" v-if="userStore.permissions.includes('edit_css_sheet') || userStore.permissions.includes('create_css_sheet')">
<CssSheetList showStaffActions />
</TabPanel>
</TabPanels>
@@ -31,8 +31,10 @@ import UserApplications from '@/components/staff/UserApplications.vue'
import { useI18n } from 'vue-i18n'
import StaffPmsTable from '@/components/staff_pm/StaffPmsTable.vue'
import CssSheetList from '@/components/CssSheetList.vue'
import { useUserStore } from '@/stores/user'
const { t } = useI18n()
const userStore = useUserStore()
</script>
<style scoped>

View File

@@ -1,6 +1,6 @@
<template>
<div class="actions wrapper-center">
<RouterLink to="/wiki/create-article">
<RouterLink to="/wiki/create-article" v-if="userStore.permissions.includes('create_wiki_article')">
<i class="pi pi-plus" v-tooltip.top="t('wiki.create_article')" />
</RouterLink>
<RouterLink to="/wiki/search">
@@ -9,7 +9,7 @@
</div>
<div v-if="wikiArticle" class="wiki-article">
<ContentContainer :containerTitle="wikiArticle.title">
<template v-if="userStore.class === 'staff'" #top-right>
<template v-if="userStore.permissions.includes('edit_wiki_article')" #top-right>
<RouterLink :to="`/wiki/article/${wikiArticle.id}/edit`" v-tooltip.top="t('wiki.edit_article')">
<i class="pi pi-pen-to-square" style="color: white" />
</RouterLink>