mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2024-11-27 16:20:36 +00:00
feat(settings): user
This commit is contained in:
parent
76e7ec3d74
commit
353fbd45d3
@ -36,6 +36,7 @@
|
||||
"byArtist": "By {artist}",
|
||||
"cancel": "Cancel",
|
||||
"castAndCrew": "Cast & crew",
|
||||
"channels": "Channels",
|
||||
"clipboardFail": "Failed to copy to clipboard",
|
||||
"clipboardSuccess": "Copied to clipboard",
|
||||
"clipboardUnsupported": "Copying to clipboard is unsupported in this browser",
|
||||
@ -183,6 +184,7 @@
|
||||
"readTheDocumentation": "Read the documentation",
|
||||
"reportAnIssue": "Report an issue with this client"
|
||||
},
|
||||
"liveTv": "Live TV",
|
||||
"login": {
|
||||
"addServer": "Add server",
|
||||
"changeServer": "Change server",
|
||||
@ -275,6 +277,7 @@
|
||||
"moreLikeArtist": "More like {artist}",
|
||||
"moreLikeThis": "More like this",
|
||||
"movies": "Movies",
|
||||
"music": "Music",
|
||||
"name": "Name",
|
||||
"networks": "Networks",
|
||||
"nextUp": "Next up",
|
||||
@ -429,7 +432,46 @@
|
||||
"noActivityFound": "No activities found",
|
||||
"noLogsFound": "No logs found"
|
||||
},
|
||||
"settings": "Settings"
|
||||
"settings": "Settings",
|
||||
"users": {
|
||||
"avatar": "Avatar",
|
||||
"edit": "Edit user",
|
||||
"lastActivityDate": "Last seen",
|
||||
"libraryAccess": "Library access",
|
||||
"libraryAccessNote": "Select the libraries to share with this user. Administrators will be able to edit all folders using the metadata manager.",
|
||||
"new": "Create user",
|
||||
"tabs": {
|
||||
"profile": {
|
||||
"allowRemoteConnections": "Allow remote connections to this server",
|
||||
"deleteUser": "Delete user",
|
||||
"deleteUserConfirm": "Are you sure you want to delete this user?",
|
||||
"name": "Name",
|
||||
"profile": "Profile"
|
||||
},
|
||||
"access": {
|
||||
"access": "Access",
|
||||
"allLibraries": "Enable access to all libraries"
|
||||
},
|
||||
"parentalControl": {
|
||||
"addBlockedTag": "Add blocked tag",
|
||||
"blockUnratedItems": "Block items with no or unregocnized rating information:",
|
||||
"blockTags": "Block items with tags:",
|
||||
"maxAllowedRating": "Maximum allowed parental rating",
|
||||
"maxAllowedRatingSubtitle": "Content with a higher rating will be hidden from this user.",
|
||||
"parentalControl": "Parental Control",
|
||||
"tagName": "Tag name",
|
||||
"unblockTag": "Unblock tag"
|
||||
},
|
||||
"password": {
|
||||
"confirmPassword": "Confirm new password",
|
||||
"currentPassword": "Current password",
|
||||
"newPassword": "New password",
|
||||
"password": "Password",
|
||||
"resetPassword": "Reset password"
|
||||
}
|
||||
},
|
||||
"users": "Users"
|
||||
}
|
||||
},
|
||||
"settingsSections": {
|
||||
"account": {
|
||||
|
@ -225,7 +225,7 @@ const adminSections = computed(() => {
|
||||
icon: IMdiAccountMultiple,
|
||||
name: t('settingsSections.users.name'),
|
||||
description: t('settingsSections.users.description'),
|
||||
link: undefined
|
||||
link: '/settings/users'
|
||||
},
|
||||
{
|
||||
icon: IMdiKeyChain,
|
||||
|
532
frontend/src/pages/settings/users/_userId/index.vue
Normal file
532
frontend/src/pages/settings/users/_userId/index.vue
Normal file
@ -0,0 +1,532 @@
|
||||
<template>
|
||||
<SettingsPage page-title="settings.users.users">
|
||||
<template #actions>
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
href="https://jellyfin.org/docs/general/server/users/"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank">
|
||||
{{ t('settings.help') }}
|
||||
</VBtn>
|
||||
</template>
|
||||
<template #content>
|
||||
<VCard
|
||||
width="100%"
|
||||
height="100%">
|
||||
<VTabs
|
||||
v-model="tab"
|
||||
color="deep-purple-accent-4"
|
||||
align-tabs="center">
|
||||
<VTab :value="1">
|
||||
{{ t("settings.users.tabs.profile.profile") }}
|
||||
</VTab>
|
||||
<VTab :value="2">
|
||||
{{ t("settings.users.tabs.access.access") }}
|
||||
</VTab>
|
||||
<VTab :value="3">
|
||||
{{ t("settings.users.tabs.parentalControl.parentalControl") }}
|
||||
</VTab>
|
||||
<VTab :value="4">
|
||||
{{ t("settings.users.tabs.password.password") }}
|
||||
</VTab>
|
||||
</VTabs>
|
||||
<VWindow v-model="tab">
|
||||
<VWindowItem
|
||||
:key="1"
|
||||
:value="1">
|
||||
<VForm>
|
||||
<VContainer>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTextField
|
||||
v-model="initializedUser.Name"
|
||||
:label="t('settings.users.tabs.profile.name')"
|
||||
hide-details />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VBtn
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="saveProfile">
|
||||
{{ t('save') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VBtn
|
||||
:loading="loading"
|
||||
color="error"
|
||||
variant="elevated"
|
||||
@click="deleteUser">
|
||||
{{ t('settings.users.tabs.profile.deleteUser') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem
|
||||
:key="2"
|
||||
:value="2">
|
||||
<VForm>
|
||||
<VContainer>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="initializedUser.CanAccessAllLibraries"
|
||||
:label="t('settings.users.tabs.access.allLibraries')" />
|
||||
<VCol />
|
||||
</vcol>
|
||||
</VRow>
|
||||
<div v-if="!initializedUser.CanAccessAllLibraries && libraries">
|
||||
<VRow>
|
||||
<div
|
||||
class="text-subtitle-1 text--secondary font-weight-medium text-capitalize">
|
||||
{{ $t('settingsSections.libraries.name') }}
|
||||
</div>
|
||||
</VRow>
|
||||
<VRow
|
||||
v-for="library of libraries.Items"
|
||||
:key="library.Id">
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="initializedUser.Folders"
|
||||
:value="library.Id"
|
||||
:label="library.Name!" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VBtn
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="saveAccess">
|
||||
{{ t('save') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem
|
||||
:key="3"
|
||||
:value="3">
|
||||
<VForm>
|
||||
<VContainer>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VSelect
|
||||
v-model="initializedUser.maxParentalRating"
|
||||
:label="t('settings.users.tabs.parentalControl.maxAllowedRating')"
|
||||
:items="parentalCategories"
|
||||
item-title="label"
|
||||
item-value="id"
|
||||
hide-details
|
||||
clearable />
|
||||
<div
|
||||
class="text-subtitle-1 text-warning font-weight-medium">
|
||||
{{ $t('settings.users.tabs.parentalControl.maxAllowedRatingSubtitle') }}
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<div
|
||||
class="text-subtitle-1 text--secondary font-weight-medium text-capitalize">
|
||||
{{ $t('settings.users.tabs.parentalControl.blockUnratedItems') }}
|
||||
</div>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VContainer>
|
||||
<VRow dense>
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="initializedUser.BlockUnratedItems"
|
||||
:label="t('books')"
|
||||
value="Book"
|
||||
density="compact" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow dense>
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="initializedUser.BlockUnratedItems"
|
||||
:label="t('channels')"
|
||||
value="ChannelContent"
|
||||
density="compact" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow dense>
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="initializedUser.BlockUnratedItems"
|
||||
:label="t('liveTv')"
|
||||
value="LiveTvChannel"
|
||||
density="compact" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow dense>
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="initializedUser.BlockUnratedItems"
|
||||
:label="t('movies')"
|
||||
value="Movie"
|
||||
density="compact" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow dense>
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="initializedUser.BlockUnratedItems"
|
||||
:label="t('music')"
|
||||
value="Music"
|
||||
density="compact" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow dense>
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="initializedUser.BlockUnratedItems"
|
||||
:label="t('trailer')"
|
||||
value="Trailer"
|
||||
density="compact" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow dense>
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="initializedUser.BlockUnratedItems"
|
||||
:label="t('shows')"
|
||||
value="Series"
|
||||
density="compact" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
<VContainer>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<div
|
||||
class="text-title font-weight-medium text-capitalize">
|
||||
{{ t('settings.users.tabs.parentalControl.blockTags') }}
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VBtn @click="addTagDialogOpen = true">
|
||||
{{ t('settings.users.tabs.parentalControl.addBlockedTag') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow
|
||||
v-for="blockedTag of initializedUser.BlockedTags"
|
||||
:key="blockedTag">
|
||||
<VCol>
|
||||
<div
|
||||
class="text-subtitle-1 font-weight-medium text-capitalize">
|
||||
{{ blockedTag }}
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VBtn
|
||||
:disabled="loading"
|
||||
color="error"
|
||||
@click="() => initializedUser.BlockedTags = initializedUser.BlockedTags.filter(tag => tag !== blockedTag)">
|
||||
{{ t('settings.users.tabs.parentalControl.unblockTag') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VBtn
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="saveParentalControl">
|
||||
{{ t('save') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
<VWindowItem
|
||||
:key="4"
|
||||
:value="4">
|
||||
<VForm>
|
||||
<VContainer>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTextField
|
||||
v-if="user.HasPassword"
|
||||
v-model="initializedUser.CurrentPassword"
|
||||
:disabled="loading"
|
||||
:label="t('settings.users.tabs.password.currentPassword')"
|
||||
hide-details />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTextField
|
||||
v-model="initializedUser.Password"
|
||||
:disabled="loading"
|
||||
:label="t('settings.users.tabs.password.newPassword')"
|
||||
hide-details />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTextField
|
||||
v-model="initializedUser.ConfirmPassword"
|
||||
:disabled="loading"
|
||||
:label="t('settings.users.tabs.password.confirmPassword')"
|
||||
hide-details />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VBtn
|
||||
v-if="user.HasPassword"
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
variant="elevated"
|
||||
color="error"
|
||||
@click="resetPassword">
|
||||
{{ t('settings.users.tabs.password.resetPassword') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
<VCol>
|
||||
<VBtn
|
||||
:disabled="loading"
|
||||
variant="elevated"
|
||||
color="primary"
|
||||
@click="submitPassword">
|
||||
{{ t('save') }}
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VContainer>
|
||||
</VForm>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
</VCard>
|
||||
<VDialog
|
||||
v-model="addTagDialogOpen"
|
||||
width="auto">
|
||||
<VCol class="pa-0 add-key-dialog">
|
||||
<VCard>
|
||||
<VCardTitle>{{ t('settings.users.tabs.parentalControl.addBlockedTag') }}</VCardTitle>
|
||||
<VCardActions>
|
||||
<VForm
|
||||
class="add-key-form"
|
||||
@submit.prevent="initializedUser.BlockedTags.push(newTagValue); addTagDialogOpen = false; newTagValue = '';">
|
||||
<VTextField
|
||||
v-model="newTagValue"
|
||||
variant="outlined"
|
||||
:label="t('settings.users.tabs.parentalControl.tagName')" />
|
||||
<VBtn
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="newTagValue === ''"
|
||||
@click="initializedUser.BlockedTags.push(newTagValue); addTagDialogOpen = false; newTagValue = '';">
|
||||
{{ $t('confirm') }}
|
||||
</VBtn>
|
||||
<VBtn @click="() => {addTagDialogOpen = false}">
|
||||
{{ $t('cancel') }}
|
||||
</VBtn>
|
||||
</VForm>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VDialog>
|
||||
</template>
|
||||
</SettingsPage>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
admin: true
|
||||
</route>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BaseItemDtoQueryResult,
|
||||
UnratedItem,
|
||||
UserDto
|
||||
} from '@jellyfin/sdk/lib/generated-client';
|
||||
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute , useRouter } from 'vue-router';
|
||||
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
|
||||
import { getLocalizationApi } from '@jellyfin/sdk/lib/utils/api/localization-api';
|
||||
import { useConfirmDialog, useRemote } from '@/composables';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const remote = useRemote();
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const addTagDialogOpen = ref<boolean>(false);
|
||||
const newTagValue = ref<string>('');
|
||||
const user = ref<UserDto>({});
|
||||
const libraries = ref<BaseItemDtoQueryResult>();
|
||||
const parentalCategories = ref<{ label: string, id: number | undefined }[]>([]);
|
||||
const initializedUser = ref<{ Name: string, CurrentPassword: string, Password: string, ConfirmPassword: string, CanAccessAllLibraries: boolean, Folders: string[], maxParentalRating: number | undefined, BlockUnratedItems: UnratedItem[], BlockedTags: string[] }>({Name: '', CurrentPassword: '', Password: '', ConfirmPassword: '', CanAccessAllLibraries: false, Folders: [], maxParentalRating: undefined, BlockUnratedItems: [], BlockedTags: []});
|
||||
const tab = ref<number>(1);
|
||||
|
||||
onMounted(async () => {
|
||||
const { userId } = route.params as { userId: string };
|
||||
|
||||
user.value = (await remote.sdk.newUserApi(getUserApi).getUserById({
|
||||
userId: userId
|
||||
})).data;
|
||||
initializeUser();
|
||||
libraries.value = (await remote.sdk.newUserApi(getLibraryApi).getMediaFolders({isHidden: false})).data;
|
||||
|
||||
const cats = (await remote.sdk.newUserApi(getLocalizationApi).getParentalRatings()).data;
|
||||
|
||||
for (const cat of cats) {
|
||||
if (parentalCategories.value.some((c) => c.id === cat.Value!)) {
|
||||
parentalCategories.value = parentalCategories.value.map((c) => {
|
||||
if (c.id === cat.Value!) {
|
||||
return {label: `${c.label}/${cat.Name!}`, id: cat.Value};
|
||||
}
|
||||
|
||||
return c;
|
||||
});
|
||||
} else {
|
||||
parentalCategories.value.push({label: cat.Name!, id: cat.Value!});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Saves the changed user access
|
||||
*/
|
||||
async function saveAccess(): Promise<void> {
|
||||
if (!user.value.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
console.log(initializedUser.value.Folders);
|
||||
await remote.sdk.newUserApi(getUserApi).updateUserPolicy({
|
||||
userId: user.value.Id,
|
||||
userPolicy: {...user.value.Policy, EnableAllFolders: initializedUser.value.CanAccessAllLibraries, EnabledFolders: initializedUser.value.Folders}
|
||||
});
|
||||
await refreshData();
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the changed profile
|
||||
*/
|
||||
async function saveProfile(): Promise<void> {
|
||||
if (!user.value.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
await remote.sdk.newUserApi(getUserApi).updateUser({
|
||||
userId: user.value.Id,
|
||||
userDto: {...user.value, Name: initializedUser.value.Name}
|
||||
});
|
||||
await refreshData();
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the changed parental control
|
||||
*/
|
||||
async function saveParentalControl(): Promise<void> {
|
||||
if (!user.value.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
await remote.sdk.newUserApi(getUserApi).updateUserPolicy({
|
||||
userId: user.value.Id,
|
||||
userPolicy: {
|
||||
...user.value.Policy,
|
||||
MaxParentalRating: initializedUser.value.maxParentalRating,
|
||||
BlockUnratedItems: initializedUser.value.BlockUnratedItems
|
||||
}
|
||||
});
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the changed password
|
||||
*/
|
||||
async function submitPassword(): Promise<void> {
|
||||
if (!user.value.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initializedUser.value.Password || initializedUser.value.Password !== initializedUser.value.ConfirmPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
await remote.sdk.newUserApi(getUserApi).updateUserPassword({userId: user.value.Id, updateUserPassword: {NewPw: initializedUser.value.Password, ...(user.value.HasPassword && {CurrentPw: initializedUser.value.ConfirmPassword})}});
|
||||
initializedUser.value = {...initializedUser.value, CurrentPassword: '',Password: '', ConfirmPassword: ''};
|
||||
await refreshData();
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the user data
|
||||
*/
|
||||
async function refreshData(): Promise<void> {
|
||||
if (!user.value.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
user.value = (await remote.sdk.newUserApi(getUserApi).getUserById({
|
||||
userId: user.value.Id
|
||||
})).data;
|
||||
initializeUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the user
|
||||
*/
|
||||
async function deleteUser():Promise<void> {
|
||||
await useConfirmDialog(async () => {
|
||||
await remote.sdk.newUserApi(getUserApi).deleteUser({userId: user.value.Id!});
|
||||
await router.push('/settings/users');
|
||||
}, {
|
||||
title: t('settings.users.tabs.profile.deleteUser'),
|
||||
text: t('settings.users.tabs.profile.deleteUserConfirm'),
|
||||
confirmText: t('delete')
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the password
|
||||
*/
|
||||
async function resetPassword(): Promise<void> {
|
||||
if (!user.value.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
await remote.sdk.newUserApi(getUserApi).updateUserPassword({userId: user.value.Id, updateUserPassword: {
|
||||
ResetPassword: true
|
||||
}});
|
||||
await refreshData();
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function makes sure that all properties are defined
|
||||
*/
|
||||
function initializeUser(): void {
|
||||
initializedUser.value = {...initializedUser.value, CanAccessAllLibraries:user.value.Policy?.EnableAllFolders ?? false, Name: user.value.Name ?? '', Folders: user.value.Policy?.EnabledFolders ?? [], maxParentalRating: user.value.Policy?.MaxParentalRating ?? undefined, BlockUnratedItems: user.value.Policy?.BlockUnratedItems?? [], BlockedTags: user.value.Policy?.BlockedTags ?? []};
|
||||
}
|
||||
</script>
|
106
frontend/src/pages/settings/users/index.vue
Normal file
106
frontend/src/pages/settings/users/index.vue
Normal file
@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<SettingsPage page-title="settings.users.users">
|
||||
<template #actions>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
class="ml-a"
|
||||
@click="$router.push('/settings/users/new')">
|
||||
{{ t('settings.users.new') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
href="https://jellyfin.org/docs/general/server/users/adding-managing-users"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank">
|
||||
{{ t('settings.help') }}
|
||||
</VBtn>
|
||||
</template>
|
||||
<template #content>
|
||||
<VCol>
|
||||
<VTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="{ text, value } in headers"
|
||||
:id="value"
|
||||
:key="value">
|
||||
{{ text }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
<!--for avatar-->
|
||||
</th>
|
||||
<th scope="col">
|
||||
<!--for edit btn-->
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="user in users"
|
||||
:key="user.Id ?? undefined">
|
||||
<td
|
||||
v-for="{ value } in headers"
|
||||
:key="value">
|
||||
{{
|
||||
value !== 'LastActivityDate'
|
||||
? user[value]
|
||||
: useDateFns(
|
||||
formatRelative,
|
||||
parseJSON(user[value] ?? "2023-02-02T02:02:02Z"), //FIXME: using 'unknown' doesnt work here? its used somewhere else in the code as well, does that even work there?
|
||||
new Date()
|
||||
).value
|
||||
}}
|
||||
</td>
|
||||
<td>
|
||||
<VImg
|
||||
:src="`${remote.sdk.api?.basePath}/Users/${user['Id']}/Images/Primary?height=50&tag=${user['PrimaryImageTag']}&quality=90`"
|
||||
height="50px"
|
||||
width="auto" />
|
||||
</td>
|
||||
<td>
|
||||
<VBtn
|
||||
color="primary"
|
||||
@click="$router.push(`/settings/users/${user['Id']}`)">
|
||||
{{ t('settings.users.edit') }}
|
||||
</VBtn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VCol>
|
||||
</template>
|
||||
</SettingsPage>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
admin: true
|
||||
</route>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||
import { formatRelative, parseJSON } from 'date-fns';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useDateFns, useRemote } from '@/composables';
|
||||
|
||||
const { t } = useI18n();
|
||||
const remote = useRemote();
|
||||
|
||||
const users = ref(
|
||||
(await remote.sdk.newUserApi(getUserApi).getUsers()).data ?? []
|
||||
);
|
||||
|
||||
const headers = computed<{ text: string; value: keyof UserDto }[]>(() => [
|
||||
{
|
||||
text: t('username'),
|
||||
value: 'Name'
|
||||
},
|
||||
{
|
||||
text: t('settings.users.lastActivityDate'),
|
||||
value: 'LastActivityDate'
|
||||
}
|
||||
]);
|
||||
</script>
|
130
frontend/src/pages/settings/users/new.vue
Normal file
130
frontend/src/pages/settings/users/new.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<SettingsPage page-title="settings.users.new">
|
||||
<template #actions>
|
||||
<VBtn
|
||||
variant="elevated"
|
||||
href="https://jellyfin.org/docs/general/server/users/"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank">
|
||||
{{ t('settings.help') }}
|
||||
</VBtn>
|
||||
</template>
|
||||
<template #content>
|
||||
<VCard
|
||||
width="100%"
|
||||
height="100%">
|
||||
<VForm
|
||||
class="py-5 px-2"
|
||||
@submit.prevent="createUser">
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTextField
|
||||
v-model="name"
|
||||
:label="t('name')" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<VTextField
|
||||
v-model="password"
|
||||
:label="t('password')" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VCol>
|
||||
<div class="text-subtitle-1 font-weight-medium text-capitalize">
|
||||
{{ t('settings.users.libraryAccess') }}
|
||||
</div>
|
||||
<VCheckbox
|
||||
v-model="canAccessAllLibraries"
|
||||
:label="t('settings.users.tabs.access.allLibraries')" />
|
||||
<VCard
|
||||
v-if="!canAccessAllLibraries"
|
||||
:title="t('libraries')"
|
||||
variant="tonal">
|
||||
<VRow
|
||||
v-for="library in libraries?.Items"
|
||||
:key="library.Id">
|
||||
<VCol>
|
||||
<VCheckbox
|
||||
v-model="accessableLibraries"
|
||||
:label="library.Name!"
|
||||
:value="library.Id" />
|
||||
</VCol>
|
||||
</VRow>
|
||||
<div class="text-capitalize text-warning ml-2">
|
||||
{{ t('settings.users.libraryAccessNote') }}
|
||||
</div>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VRow>
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
:loading="loading"
|
||||
@click="createUser">
|
||||
{{ t('settings.users.new') }}
|
||||
</VBtn>
|
||||
</VRow>
|
||||
</VForm>
|
||||
</VCard>
|
||||
</template>
|
||||
</SettingsPage>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
admin: true
|
||||
</route>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRemote } from '@/composables';
|
||||
|
||||
const { t } = useI18n();
|
||||
const remote = useRemote();
|
||||
const router = useRouter();
|
||||
const name = ref('');
|
||||
const password = ref('');
|
||||
const canAccessAllLibraries = ref(true);
|
||||
const accessableLibraries = ref<string[]>([]);
|
||||
const libraries = ref<BaseItemDtoQueryResult>();
|
||||
const loading = ref(false);
|
||||
|
||||
libraries.value = (await remote.sdk.newUserApi(getLibraryApi).getMediaFolders({isHidden: false})).data;
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
*/
|
||||
async function createUser(): Promise<void> {
|
||||
loading.value = true;
|
||||
|
||||
// Create the user
|
||||
const res = (await remote.sdk.newUserApi(getUserApi).createUserByName({
|
||||
createUserByName: {
|
||||
Name: name.value
|
||||
}
|
||||
})).data;
|
||||
|
||||
// Set the library access policy
|
||||
await remote.sdk.newUserApi(getUserApi).updateUserPolicy({
|
||||
userId: res.Id!,
|
||||
userPolicy: {
|
||||
...res.Policy,
|
||||
EnableAllFolders: canAccessAllLibraries.value,
|
||||
...(!canAccessAllLibraries.value && {
|
||||
EnabledFolders: accessableLibraries.value
|
||||
})
|
||||
}
|
||||
});
|
||||
loading.value = false;
|
||||
await router.push(`/settings/users/${res.Id}`);
|
||||
}
|
||||
|
||||
</script>
|
3
frontend/types/global/routes.d.ts
vendored
3
frontend/types/global/routes.d.ts
vendored
@ -58,6 +58,9 @@ declare module 'vue-router/auto/routes' {
|
||||
'/settings/apikeys': RouteRecordInfo<'/settings/apikeys', '/settings/apikeys', Record<never, never>, Record<never, never>>,
|
||||
'/settings/devices': RouteRecordInfo<'/settings/devices', '/settings/devices', Record<never, never>, Record<never, never>>,
|
||||
'/settings/logs-and-activity': RouteRecordInfo<'/settings/logs-and-activity', '/settings/logs-and-activity', Record<never, never>, Record<never, never>>,
|
||||
'/settings/users/': RouteRecordInfo<'/settings/users/', '/settings/users', Record<never, never>, Record<never, never>>,
|
||||
'/settings/users/_userId/': RouteRecordInfo<'/settings/users/_userId/', '/settings/users/_userId', Record<never, never>, Record<never, never>>,
|
||||
'/settings/users/new': RouteRecordInfo<'/settings/users/new', '/settings/users/new', Record<never, never>, Record<never, never>>,
|
||||
'/wizard': RouteRecordInfo<'/wizard', '/wizard', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user