diff --git a/apps/app-frontend/src/components/ui/skin-site/SkinSiteAccountCard.vue b/apps/app-frontend/src/components/ui/skin-site/SkinSiteAccountCard.vue index 8720deb..9e9fcdb 100644 --- a/apps/app-frontend/src/components/ui/skin-site/SkinSiteAccountCard.vue +++ b/apps/app-frontend/src/components/ui/skin-site/SkinSiteAccountCard.vue @@ -30,15 +30,23 @@ const messages = defineMessages({ error: { id: 'app.skin-site.home.error', defaultMessage: 'Could not load players.' }, signIn: { id: 'app.skin-site.home.sign-in', defaultMessage: 'Sign in' }, settings: { id: 'app.skin-site.home.settings', defaultMessage: 'Account' }, + admin: { id: 'app.skin-site.home.admin', defaultMessage: 'Admin' }, myPlayers: { id: 'app.skin-site.home.my-players', defaultMessage: 'My players' }, }) -const emit = defineEmits<{ (e: 'sign-in' | 'open-settings' | 'open-players'): void }>() +const emit = defineEmits<{ + (e: 'sign-in' | 'open-settings' | 'open-players' | 'open-admin'): void +}>() const luck = ref(null) const luckLoading = ref(false) const signedIn = computed(() => skinSiteStatus.value === 'signed-in' && !!skinSiteUser.value) + +const isAdmin = computed(() => { + const permissions = skinSiteUser.value?.permissions ?? [] + return permissions.includes('WEB_ADMIN') || permissions.includes('SERVER_ADMIN') +}) const players = computed(() => skinSitePlayers.value) const playersLoading = computed( () => skinSitePlayersStatus.value === 'checking' || skinSitePlayersStatus.value === 'idle', @@ -98,6 +106,14 @@ watch(signedIn, loadLuck) > {{ formatMessage(messages.settings) }} +
+import { defineMessages, useVIntl } from '@modrinth/ui' +import { computed, ref } from 'vue' + +import { skinSiteUser } from '@/composables/skin-site-session' + +const emit = defineEmits<{ (e: 'back'): void }>() + +const { formatMessage } = useVIntl() + +const messages = defineMessages({ + title: { id: 'app.skin-site.admin.title', defaultMessage: 'Admin console' }, + back: { id: 'app.skin-site.admin.back', defaultMessage: 'Back to home' }, + noAccess: { id: 'app.skin-site.admin.no-access', defaultMessage: 'You do not have access.' }, + noAccessHint: { + id: 'app.skin-site.admin.no-access-hint', + defaultMessage: 'Sign in with an administrator account to use this console.', + }, + groupSecurity: { id: 'app.skin-site.admin.group.security', defaultMessage: 'Security' }, + groupUsers: { id: 'app.skin-site.admin.group.users', defaultMessage: 'Users' }, + groupCommunity: { id: 'app.skin-site.admin.group.community', defaultMessage: 'Community' }, + groupResources: { id: 'app.skin-site.admin.group.resources', defaultMessage: 'Resources' }, + itemServers: { id: 'app.skin-site.admin.item.servers', defaultMessage: 'Servers' }, + itemSummary: { id: 'app.skin-site.admin.item.summary', defaultMessage: 'Summary' }, + itemAccessAudit: { id: 'app.skin-site.admin.item.access-audit', defaultMessage: 'Access audit' }, + itemUsers: { id: 'app.skin-site.admin.item.users', defaultMessage: 'Users' }, + itemPlayers: { id: 'app.skin-site.admin.item.players', defaultMessage: 'Players' }, + itemReview: { id: 'app.skin-site.admin.item.review', defaultMessage: 'Player review' }, + itemBroadcast: { id: 'app.skin-site.admin.item.broadcast', defaultMessage: 'Broadcast' }, + itemChat: { id: 'app.skin-site.admin.item.chat', defaultMessage: 'Chat rooms' }, + itemWiki: { id: 'app.skin-site.admin.item.wiki', defaultMessage: 'Wiki' }, + itemDonations: { id: 'app.skin-site.admin.item.donations', defaultMessage: 'Donations' }, + itemMods: { id: 'app.skin-site.admin.item.mods', defaultMessage: 'Mods' }, + itemLauncher: { id: 'app.skin-site.admin.item.launcher', defaultMessage: 'Launcher' }, + itemDownloads: { id: 'app.skin-site.admin.item.downloads', defaultMessage: 'Downloads' }, +}) + +type AdminKey = + | 'servers' + | 'summary' + | 'access-audit' + | 'users' + | 'players' + | 'review' + | 'broadcast' + | 'chat' + | 'wiki' + | 'donations' + | 'mods' + | 'launcher' + | 'downloads' + +interface NavItem { + key: AdminKey + label: string +} +interface NavGroup { + id: string + label: string + items: NavItem[] +} + +const navGroups = computed(() => [ + { + id: 'security', + label: formatMessage(messages.groupSecurity), + items: [ + { key: 'servers', label: formatMessage(messages.itemServers) }, + { key: 'summary', label: formatMessage(messages.itemSummary) }, + { key: 'access-audit', label: formatMessage(messages.itemAccessAudit) }, + ], + }, + { + id: 'users', + label: formatMessage(messages.groupUsers), + items: [ + { key: 'users', label: formatMessage(messages.itemUsers) }, + { key: 'players', label: formatMessage(messages.itemPlayers) }, + { key: 'review', label: formatMessage(messages.itemReview) }, + ], + }, + { + id: 'community', + label: formatMessage(messages.groupCommunity), + items: [ + { key: 'broadcast', label: formatMessage(messages.itemBroadcast) }, + { key: 'chat', label: formatMessage(messages.itemChat) }, + { key: 'wiki', label: formatMessage(messages.itemWiki) }, + { key: 'donations', label: formatMessage(messages.itemDonations) }, + ], + }, + { + id: 'resources', + label: formatMessage(messages.groupResources), + items: [ + { key: 'mods', label: formatMessage(messages.itemMods) }, + { key: 'launcher', label: formatMessage(messages.itemLauncher) }, + { key: 'downloads', label: formatMessage(messages.itemDownloads) }, + ], + }, +]) + +const hasAdminAccess = computed(() => { + const permissions = skinSiteUser.value?.permissions ?? [] + return permissions.includes('WEB_ADMIN') || permissions.includes('SERVER_ADMIN') +}) + +const current = ref('summary') + + diff --git a/apps/app-frontend/src/components/ui/skin-site/SkinSiteHome.vue b/apps/app-frontend/src/components/ui/skin-site/SkinSiteHome.vue index 64f483d..4d626df 100644 --- a/apps/app-frontend/src/components/ui/skin-site/SkinSiteHome.vue +++ b/apps/app-frontend/src/components/ui/skin-site/SkinSiteHome.vue @@ -4,6 +4,7 @@ import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/u import { ref } from 'vue' import SkinSiteAccountCard from './SkinSiteAccountCard.vue' +import SkinSiteAdminView from './SkinSiteAdminView.vue' import SkinSiteAnnouncements from './SkinSiteAnnouncements.vue' import SkinSiteBugCreateModal from './SkinSiteBugCreateModal.vue' import SkinSiteBugDetailView from './SkinSiteBugDetailView.vue' @@ -35,7 +36,15 @@ const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil' const loginModal = ref>() const profileModal = ref>() const view = ref< - 'home' | 'players' | 'downloads' | 'map' | 'review' | 'bugs' | 'bug-detail' | 'bug-notifications' + | 'home' + | 'players' + | 'downloads' + | 'map' + | 'review' + | 'bugs' + | 'bug-detail' + | 'bug-notifications' + | 'admin' >('home') const selectedBugId = ref(null) @@ -88,6 +97,7 @@ async function copyAddress() { @back="view = 'bugs'" @open="openBug" /> +
diff --git a/apps/app-frontend/src/composables/skin-site-session.ts b/apps/app-frontend/src/composables/skin-site-session.ts index 56203cc..51f3412 100644 --- a/apps/app-frontend/src/composables/skin-site-session.ts +++ b/apps/app-frontend/src/composables/skin-site-session.ts @@ -19,7 +19,7 @@ import { fetchImageAsDataUrl, renderSkinHead } from '@/helpers/skin-site-render' export const SKIN_SITE_ORIGIN = 'https://skin.starlight.cool' -export type SkinSiteUser = { uuid: string; username: string } +export type SkinSiteUser = { uuid: string; username: string; permissions: string[] } export type SkinSiteStatus = 'checking' | 'signed-out' | 'signed-in' | 'error' export type SkinSitePlayer = { uuid: string @@ -46,7 +46,11 @@ export const skinSitePlayersStatus = readonly(playersStatus) export const selectedSkinSitePlayerId = readonly(selectedPlayerId) function toUser(raw: ApiSkinSiteUser): SkinSiteUser { - return { uuid: raw.uuid, username: raw.username } + return { + uuid: raw.uuid, + username: raw.username, + permissions: Array.isArray(raw.permissions) ? raw.permissions : [], + } } function clearSessionState() { @@ -111,7 +115,7 @@ export async function signInSkinSite(payload: { status.value = 'signed-in' void requestSkinSitePlayers().catch(() => {}) } else { - user.value = { uuid: '', username: result.payload.username } + user.value = { uuid: '', username: result.payload.username, permissions: [] } status.value = 'signed-in' } } diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 386094f..c70e893 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -11767,5 +11767,38 @@ "app.skin-site.home.subtitle": {"message": "The wardrobe for StarLight Server players."}, "app.skin-site.home.address": {"message": "Skin site address"}, "app.skin-site.home.copied": {"message": "Address copied."}, - "app.skin-site.home.copy": {"message": "Copy"} + "app.skin-site.home.copy": {"message": "Copy"}, + "app.skin-site.admin.title": {"message": "Admin console"}, + "app.skin-site.admin.back": {"message": "Back to home"}, + "app.skin-site.admin.no-access": {"message": "You do not have access."}, + "app.skin-site.admin.no-access-hint": {"message": "Sign in with an administrator account to use this console."}, + "app.skin-site.admin.group.security": {"message": "Security"}, + "app.skin-site.admin.group.users": {"message": "Users"}, + "app.skin-site.admin.group.community": {"message": "Community"}, + "app.skin-site.admin.group.resources": {"message": "Resources"}, + "app.skin-site.admin.item.servers": {"message": "Servers"}, + "app.skin-site.admin.item.summary": {"message": "Summary"}, + "app.skin-site.admin.item.access-audit": {"message": "Access audit"}, + "app.skin-site.admin.item.users": {"message": "Users"}, + "app.skin-site.admin.item.players": {"message": "Players"}, + "app.skin-site.admin.item.review": {"message": "Player review"}, + "app.skin-site.admin.item.broadcast": {"message": "Broadcast"}, + "app.skin-site.admin.item.chat": {"message": "Chat rooms"}, + "app.skin-site.admin.item.wiki": {"message": "Wiki"}, + "app.skin-site.admin.item.donations": {"message": "Donations"}, + "app.skin-site.admin.item.mods": {"message": "Mods"}, + "app.skin-site.admin.item.launcher": {"message": "Launcher"}, + "app.skin-site.admin.item.downloads": {"message": "Downloads"}, + "app.skin-site.home.admin": {"message": "Admin"}, + "app.skin-site.home.settings": {"message": "Account"}, + "app.skin-site.home.sign-in": {"message": "Sign in"}, + "app.skin-site.home.players": {"message": "Players"}, + "app.skin-site.home.my-players": {"message": "My players"}, + "app.skin-site.home.no-players": {"message": "No players linked yet."}, + "app.skin-site.home.loading": {"message": "Loading…"}, + "app.skin-site.home.error": {"message": "Could not load."}, + "app.skin-site.home.luck": {"message": "Luck"}, + "app.skin-site.home.luck-unavailable": {"message": "Unavailable"}, + "app.skin-site.home.signed-out-title": {"message": "Not signed in"}, + "app.skin-site.home.signed-out-body": {"message": "Sign in to manage your account and textures."} } diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index e9e73e1..250cfd8 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -11598,5 +11598,38 @@ "app.skin-site.home.subtitle": {"message": "星光服务器玩家的衣柜。"}, "app.skin-site.home.address": {"message": "皮肤站地址"}, "app.skin-site.home.copied": {"message": "地址已复制。"}, - "app.skin-site.home.copy": {"message": "复制"} + "app.skin-site.home.copy": {"message": "复制"}, + "app.skin-site.admin.title": {"message": "管理控制台"}, + "app.skin-site.admin.back": {"message": "返回主页"}, + "app.skin-site.admin.no-access": {"message": "你没有访问权限。"}, + "app.skin-site.admin.no-access-hint": {"message": "请使用管理员账号登录后使用本控制台。"}, + "app.skin-site.admin.group.security": {"message": "系统安全"}, + "app.skin-site.admin.group.users": {"message": "用户管理"}, + "app.skin-site.admin.group.community": {"message": "社区管理"}, + "app.skin-site.admin.group.resources": {"message": "资源分发"}, + "app.skin-site.admin.item.servers": {"message": "服务器管理"}, + "app.skin-site.admin.item.summary": {"message": "状态概览"}, + "app.skin-site.admin.item.access-audit": {"message": "登录与认证审计"}, + "app.skin-site.admin.item.users": {"message": "用户信息"}, + "app.skin-site.admin.item.players": {"message": "玩家信息"}, + "app.skin-site.admin.item.review": {"message": "玩家审核"}, + "app.skin-site.admin.item.broadcast": {"message": "公告操作"}, + "app.skin-site.admin.item.chat": {"message": "聊天室管理"}, + "app.skin-site.admin.item.wiki": {"message": "维基管理"}, + "app.skin-site.admin.item.donations": {"message": "捐赠管理"}, + "app.skin-site.admin.item.mods": {"message": "Mod 管理"}, + "app.skin-site.admin.item.launcher": {"message": "启动器管理"}, + "app.skin-site.admin.item.downloads": {"message": "下载管理"}, + "app.skin-site.home.admin": {"message": "管理"}, + "app.skin-site.home.settings": {"message": "账号"}, + "app.skin-site.home.sign-in": {"message": "登录"}, + "app.skin-site.home.players": {"message": "角色"}, + "app.skin-site.home.my-players": {"message": "我的角色"}, + "app.skin-site.home.no-players": {"message": "尚未绑定角色。"}, + "app.skin-site.home.loading": {"message": "加载中…"}, + "app.skin-site.home.error": {"message": "加载失败。"}, + "app.skin-site.home.luck": {"message": "幸运值"}, + "app.skin-site.home.luck-unavailable": {"message": "不可用"}, + "app.skin-site.home.signed-out-title": {"message": "未登录"}, + "app.skin-site.home.signed-out-body": {"message": "登录后可管理你的账号与皮肤。"} }