mirror of
https://github.com/rommapp/romm.git
synced 2025-12-22 10:27:13 +00:00
refactor: update routing to use 'home' instead of 'dashboard' and place login and setup under Auth layout
This commit is contained in:
@@ -18,25 +18,25 @@ runtimes:
|
||||
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||
lint:
|
||||
enabled:
|
||||
- markdownlint@0.42.0
|
||||
- eslint@9.14.0
|
||||
- markdownlint@0.43.0
|
||||
- eslint@9.16.0
|
||||
- actionlint@1.7.4
|
||||
- bandit@1.7.10
|
||||
- bandit@1.8.0
|
||||
- black@24.10.0
|
||||
- checkov@3.2.296
|
||||
- checkov@3.2.328
|
||||
- git-diff-check
|
||||
- isort@5.13.2
|
||||
- mypy@1.13.0
|
||||
- osv-scanner@1.9.1
|
||||
- oxipng@9.1.2
|
||||
- prettier@3.3.3
|
||||
- ruff@0.7.3
|
||||
- oxipng@9.1.3
|
||||
- prettier@3.4.2
|
||||
- ruff@0.8.1
|
||||
- shellcheck@0.10.0
|
||||
- shfmt@3.6.0
|
||||
- svgo@3.3.2
|
||||
- taplo@0.9.3
|
||||
- trivy@0.56.2
|
||||
- trufflehog@3.83.6
|
||||
- trufflehog@3.84.2
|
||||
- yamllint@1.35.1
|
||||
ignore:
|
||||
- linters: [ALL]
|
||||
|
||||
@@ -47,7 +47,7 @@ async function deleteCollection() {
|
||||
return;
|
||||
});
|
||||
|
||||
await router.push({ name: "dashboard" });
|
||||
await router.push({ name: "home" });
|
||||
|
||||
collectionsStore.remove(collection.value);
|
||||
emitter?.emit("refreshDrawer", null);
|
||||
|
||||
@@ -66,7 +66,7 @@ async function removeRomsFromCollection() {
|
||||
emitter?.emit("showLoadingDialog", { loading: false, scrim: false });
|
||||
romsStore.resetSelection();
|
||||
if (selectedCollection.value?.roms.length == 0) {
|
||||
router.push({ name: "dashboard" });
|
||||
router.push({ name: "home" });
|
||||
}
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ async function deletePlatform() {
|
||||
return;
|
||||
});
|
||||
|
||||
await router.push({ name: "dashboard" });
|
||||
await router.push({ name: "home" });
|
||||
|
||||
platformsStore.remove(platform.value);
|
||||
emitter?.emit("refreshDrawer", null);
|
||||
|
||||
34
frontend/src/layouts/Auth.vue
Normal file
34
frontend/src/layouts/Auth.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import Notification from "@/components/common/Notifications/Notification.vue";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
|
||||
// Props
|
||||
const heartbeatStore = storeHeartbeat();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="h-100 w-100 position-absolute" id="bg" />
|
||||
<notification />
|
||||
<v-container class="fill-height justify-center">
|
||||
<router-view />
|
||||
</v-container>
|
||||
<div id="version" class="position-absolute">
|
||||
<span class="text-white text-shadow">{{
|
||||
heartbeatStore.value.VERSION
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#bg {
|
||||
background: url("/assets/login_bg.png") center center;
|
||||
background-size: cover;
|
||||
}
|
||||
#version {
|
||||
text-shadow:
|
||||
1px 1px 1px #000000,
|
||||
0 0 1px #000000;
|
||||
bottom: 0.3rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@ import "@/styles/scrollbar.css";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import mitt from "mitt";
|
||||
import { createApp } from "vue";
|
||||
import App from "./RomM.vue";
|
||||
import App from "@/RomM.vue";
|
||||
|
||||
const emitter = mitt<Events>();
|
||||
const app = createApp(App);
|
||||
|
||||
@@ -5,23 +5,37 @@ import storeHeartbeat from "@/stores/heartbeat";
|
||||
const routes = [
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: () => import("@/views/Login.vue"),
|
||||
name: "loginView",
|
||||
component: () => import("@/layouts/Auth.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "login",
|
||||
component: () => import("@/views/Auth/Login.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/setup",
|
||||
name: "setup",
|
||||
component: () => import("@/views/Setup.vue"),
|
||||
name: "setupView",
|
||||
component: () => import("@/layouts/Auth.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "setup",
|
||||
component: () => import("@/views/Auth/Setup.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
name: "main",
|
||||
component: () => import("@/layouts/Main.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "dashboard",
|
||||
component: () => import("@/views/Dashboard.vue"),
|
||||
name: "home",
|
||||
component: () => import("@/views/Home.vue"),
|
||||
},
|
||||
{
|
||||
path: "platform/:platform",
|
||||
@@ -41,12 +55,12 @@ const routes = [
|
||||
{
|
||||
path: "rom/:rom/ejs",
|
||||
name: "emulatorjs",
|
||||
component: () => import("@/views/EmulatorJS/Base.vue"),
|
||||
component: () => import("@/views/Player/EmulatorJS/Base.vue"),
|
||||
},
|
||||
{
|
||||
path: "rom/:rom/ruffle",
|
||||
name: "ruffle",
|
||||
component: () => import("@/views/RuffleRS/Base.vue"),
|
||||
component: () => import("@/views/Player/RuffleRS/Base.vue"),
|
||||
},
|
||||
{
|
||||
path: "scan",
|
||||
@@ -66,19 +80,19 @@ const routes = [
|
||||
{
|
||||
path: "management",
|
||||
name: "management",
|
||||
component: () => import("@/views/Management.vue"),
|
||||
component: () => import("@/views/Settings/Management.vue"),
|
||||
},
|
||||
{
|
||||
path: "administration",
|
||||
name: "administration",
|
||||
component: () => import("@/views/Administration.vue"),
|
||||
component: () => import("@/views/Settings/Administration.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":pathMatch(.*)*",
|
||||
name: "noMatch",
|
||||
component: () => import("@/views/Dashboard.vue"),
|
||||
component: () => import("@/views/Home.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -89,10 +103,10 @@ const router = createRouter({
|
||||
routes,
|
||||
});
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const heartbeat = storeHeartbeat();
|
||||
if (to.name == "setup" && !heartbeat.value.SHOW_SETUP_WIZARD) {
|
||||
next({ name: "dashboard" });
|
||||
next({ name: "home" });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default defineStore("navigation", {
|
||||
},
|
||||
goHome() {
|
||||
this.resetDrawers();
|
||||
this.$router.push({ name: "dashboard" });
|
||||
this.$router.push({ name: "home" });
|
||||
},
|
||||
goScan() {
|
||||
this.resetDrawers();
|
||||
|
||||
106
frontend/src/views/Auth/Login.vue
Normal file
106
frontend/src/views/Auth/Login.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import identityApi from "@/services/api/identity";
|
||||
import { refetchCSRFToken } from "@/services/api/index";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
// Props
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const router = useRouter();
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const visiblePassword = ref(false);
|
||||
const logging = ref(false);
|
||||
|
||||
// Functions
|
||||
async function login() {
|
||||
logging.value = true;
|
||||
|
||||
await identityApi
|
||||
.login(username.value, password.value)
|
||||
.then(async () => {
|
||||
// Refetch CSRF token
|
||||
await refetchCSRFToken();
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
router.push(params.get("next") ?? "/");
|
||||
})
|
||||
.catch(({ response, message }) => {
|
||||
const errorMessage =
|
||||
response.data?.detail ||
|
||||
response.data ||
|
||||
message ||
|
||||
response.statusText;
|
||||
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Unable to login: ${errorMessage}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
});
|
||||
|
||||
console.error(
|
||||
`[${response.status} ${response.statusText}] ${errorMessage}`,
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
logging.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="translucent-dark py-8 px-5" width="500">
|
||||
<v-img src="/assets/isotipo.svg" class="mx-auto" width="150" />
|
||||
<v-row class="text-white justify-center mt-2" no-gutters>
|
||||
<v-col cols="10">
|
||||
<v-form @submit.prevent="login">
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
autocomplete="on"
|
||||
required
|
||||
prepend-inner-icon="mdi-account"
|
||||
type="text"
|
||||
label="Username"
|
||||
variant="underlined"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
autocomplete="on"
|
||||
required
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:type="visiblePassword ? 'text' : 'password'"
|
||||
label="Password"
|
||||
variant="underlined"
|
||||
:append-inner-icon="visiblePassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click:append-inner="visiblePassword = !visiblePassword"
|
||||
/>
|
||||
<v-btn
|
||||
type="submit"
|
||||
:disabled="logging || !username || !password"
|
||||
:variant="!username || !password ? 'text' : 'flat'"
|
||||
class="bg-terciary"
|
||||
block
|
||||
:loading="logging"
|
||||
>
|
||||
<span>Login</span>
|
||||
<template #append>
|
||||
<v-icon class="text-romm-accent-1"
|
||||
>mdi-chevron-right-circle-outline</v-icon
|
||||
>
|
||||
</template>
|
||||
<template #loader>
|
||||
<v-progress-circular
|
||||
color="romm-accent-1"
|
||||
:width="2"
|
||||
:size="20"
|
||||
indeterminate
|
||||
/>
|
||||
</template>
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
191
frontend/src/views/Auth/Setup.vue
Normal file
191
frontend/src/views/Auth/Setup.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import router from "@/plugins/router";
|
||||
import { refetchCSRFToken } from "@/services/api/index";
|
||||
import userApi from "@/services/api/user";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Props
|
||||
const { xs } = useDisplay();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const heartbeat = storeHeartbeat();
|
||||
const visiblePassword = ref(false);
|
||||
// Use a computed property to reactively update metadataOptions based on heartbeat
|
||||
const metadataOptions = computed(() => [
|
||||
{
|
||||
name: "IGDB",
|
||||
value: "igdb",
|
||||
logo_path: "/assets/scrappers/igdb.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.IGDB_API_ENABLED,
|
||||
},
|
||||
{
|
||||
name: "MobyGames",
|
||||
value: "moby",
|
||||
logo_path: "/assets/scrappers/moby.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.MOBY_API_ENABLED,
|
||||
},
|
||||
{
|
||||
name: "SteamgridDB",
|
||||
value: "sgdb",
|
||||
logo_path: "/assets/scrappers/sgdb.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.STEAMGRIDDB_ENABLED,
|
||||
},
|
||||
]);
|
||||
const defaultAdminUser = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
role: "admin",
|
||||
});
|
||||
const step = ref(1); // 1: Create admin user, 2: Check metadata sources, 3: Finish
|
||||
const filledAdminUser = computed(
|
||||
() =>
|
||||
defaultAdminUser.value.username != "" &&
|
||||
defaultAdminUser.value.password != "",
|
||||
);
|
||||
const isFirstStep = computed(() => step.value == 1);
|
||||
const isLastStep = computed(() => step.value == 2);
|
||||
|
||||
// Functions
|
||||
async function finishWizard() {
|
||||
await userApi
|
||||
.createUser(defaultAdminUser.value)
|
||||
.then(async () => {
|
||||
await refetchCSRFToken();
|
||||
router.push({ name: "login" });
|
||||
})
|
||||
.catch(({ response, message }) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Unable to create user: ${
|
||||
response?.data?.detail || response?.statusText || message
|
||||
}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="translucent-dark px-3" width="700">
|
||||
<v-img src="/assets/isotipo.svg" class="mx-auto mt-6" width="70" />
|
||||
<v-stepper :mobile="xs" class="bg-transparent" v-model="step" flat>
|
||||
<template v-slot:default="{ prev, next }">
|
||||
<v-stepper-header>
|
||||
<v-stepper-item :value="1"
|
||||
><template #title
|
||||
><span class="text-white text-shadow"
|
||||
>Create an admin user</span
|
||||
></template
|
||||
></v-stepper-item
|
||||
>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-stepper-item :value="2"
|
||||
><template #title
|
||||
><span class="text-white text-shadow"
|
||||
>Check metadata sources</span
|
||||
></template
|
||||
></v-stepper-item
|
||||
>
|
||||
</v-stepper-header>
|
||||
|
||||
<v-stepper-window>
|
||||
<v-stepper-window-item :value="1" :key="1">
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-row v-if="xs" no-gutters class="text-center">
|
||||
<v-col>
|
||||
<span>Create an admin user</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="text-white justify-center mt-3" no-gutters>
|
||||
<v-col cols="10" md="8">
|
||||
<v-form @submit.prevent>
|
||||
<v-text-field
|
||||
v-model="defaultAdminUser.username"
|
||||
required
|
||||
prepend-inner-icon="mdi-account"
|
||||
type="text"
|
||||
label="Username"
|
||||
variant="underlined"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="defaultAdminUser.password"
|
||||
required
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:type="visiblePassword ? 'text' : 'password'"
|
||||
label="Password"
|
||||
variant="underlined"
|
||||
:append-inner-icon="
|
||||
visiblePassword ? 'mdi-eye-off' : 'mdi-eye'
|
||||
"
|
||||
@click:append-inner="visiblePassword = !visiblePassword"
|
||||
/>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-stepper-window-item>
|
||||
|
||||
<v-stepper-window-item :value="2" :key="2">
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-row v-if="xs" no-gutters class="text-center mb-6">
|
||||
<v-col>
|
||||
<span>Check metadata sources</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="justify-center align-center" no-gutters>
|
||||
<v-col :max-width="300" id="sources">
|
||||
<v-list-item
|
||||
v-for="source in metadataOptions"
|
||||
class="text-white text-shadow"
|
||||
:title="source.name"
|
||||
:subtitle="
|
||||
source.disabled ? 'API key missing or invalid' : ''
|
||||
"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar size="30" rounded="1">
|
||||
<v-img :src="source.logo_path" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
<template #append>
|
||||
<span class="ml-2" v-if="source.disabled">❌</span>
|
||||
<span class="ml-2" v-else>✅</span>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-stepper-window-item>
|
||||
</v-stepper-window>
|
||||
|
||||
<v-stepper-actions :disabled="!filledAdminUser">
|
||||
<template #prev>
|
||||
<v-btn
|
||||
class="text-white text-shadow"
|
||||
:ripple="false"
|
||||
:disabled="isFirstStep"
|
||||
@click="prev"
|
||||
>{{ isFirstStep ? "" : "previous" }}</v-btn
|
||||
>
|
||||
</template>
|
||||
<template #next>
|
||||
<v-btn
|
||||
class="text-white text-shadow"
|
||||
@click="!isLastStep ? next() : finishWizard()"
|
||||
>{{ !isLastStep ? "Next" : "Finish" }}</v-btn
|
||||
>
|
||||
</template>
|
||||
</v-stepper-actions>
|
||||
</template>
|
||||
</v-stepper>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import Collections from "@/components/Dashboard/Collections.vue";
|
||||
import Platforms from "@/components/Dashboard/Platforms.vue";
|
||||
import recentlyAdded from "@/components/Dashboard/Recent.vue";
|
||||
import Stats from "@/components/Dashboard/Stats.vue";
|
||||
import Collections from "@/components/Home/Collections.vue";
|
||||
import Platforms from "@/components/Home/Platforms.vue";
|
||||
import RecentlyAdded from "@/components/Home/Recent.vue";
|
||||
import Stats from "@/components/Home/Stats.vue";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeCollections from "@/stores/collections";
|
||||
import storePlatforms from "@/stores/platforms";
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import identityApi from "@/services/api/identity";
|
||||
import { refetchCSRFToken } from "@/services/api/index";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
// Props
|
||||
const heartbeatStore = storeHeartbeat();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const router = useRouter();
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const visiblePassword = ref(false);
|
||||
const logging = ref(false);
|
||||
|
||||
// Functions
|
||||
async function login() {
|
||||
logging.value = true;
|
||||
|
||||
await identityApi
|
||||
.login(username.value, password.value)
|
||||
.then(async () => {
|
||||
// Refetch CSRF token
|
||||
await refetchCSRFToken();
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
router.push(params.get("next") ?? "/");
|
||||
})
|
||||
.catch(({ response, message }) => {
|
||||
const errorMessage =
|
||||
response.data?.detail ||
|
||||
response.data ||
|
||||
message ||
|
||||
response.statusText;
|
||||
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Unable to login: ${errorMessage}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
});
|
||||
|
||||
console.error(
|
||||
`[${response.status} ${response.statusText}] ${errorMessage}`,
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
logging.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span id="bg" />
|
||||
|
||||
<v-container class="fill-height justify-center">
|
||||
<v-card class="translucent-dark py-8 px-5" width="500">
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-img src="/assets/isotipo.svg" class="mx-auto" width="150" />
|
||||
|
||||
<v-row class="text-white justify-center mt-2" no-gutters>
|
||||
<v-col cols="10" md="8">
|
||||
<v-form @submit.prevent>
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
autocomplete="on"
|
||||
required
|
||||
prepend-inner-icon="mdi-account"
|
||||
type="text"
|
||||
label="Username"
|
||||
variant="underlined"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
autocomplete="on"
|
||||
required
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:type="visiblePassword ? 'text' : 'password'"
|
||||
label="Password"
|
||||
variant="underlined"
|
||||
:append-inner-icon="
|
||||
visiblePassword ? 'mdi-eye-off' : 'mdi-eye'
|
||||
"
|
||||
@click:append-inner="visiblePassword = !visiblePassword"
|
||||
/>
|
||||
<v-btn
|
||||
type="submit"
|
||||
:disabled="logging || !username || !password"
|
||||
:variant="!username || !password ? 'text' : 'flat'"
|
||||
class="bg-terciary"
|
||||
block
|
||||
:loading="logging"
|
||||
@click="login()"
|
||||
>
|
||||
<span>Login</span>
|
||||
<template #append>
|
||||
<v-icon class="text-romm-accent-1"
|
||||
>mdi-chevron-right-circle-outline</v-icon
|
||||
>
|
||||
</template>
|
||||
<template #loader>
|
||||
<v-progress-circular
|
||||
color="romm-accent-1"
|
||||
:width="2"
|
||||
:size="20"
|
||||
indeterminate
|
||||
/>
|
||||
</template>
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
|
||||
<div id="version" class="position-absolute">
|
||||
<span class="text-white text-shadow">{{
|
||||
heartbeatStore.value.VERSION
|
||||
}}</span>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#bg {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: url("/assets/login_bg.png") center center;
|
||||
background-size: cover;
|
||||
}
|
||||
#version {
|
||||
bottom: 0.3rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@ import romApi from "@/services/api/rom";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import { formatBytes, formatTimestamp, getSupportedEJSCores } from "@/utils";
|
||||
import Player from "@/views/EmulatorJS/Player.vue";
|
||||
import Player from "@/views/Player/EmulatorJS/Player.vue";
|
||||
import { isNull } from "lodash";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { onMounted, ref } from "vue";
|
||||
@@ -1,238 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import router from "@/plugins/router";
|
||||
import { refetchCSRFToken } from "@/services/api/index";
|
||||
import userApi from "@/services/api/user";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Props
|
||||
const { xs, smAndDown } = useDisplay();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const heartbeat = storeHeartbeat();
|
||||
const visiblePassword = ref(false);
|
||||
// Use a computed property to reactively update metadataOptions based on heartbeat
|
||||
const metadataOptions = computed(() => [
|
||||
{
|
||||
name: "IGDB",
|
||||
value: "igdb",
|
||||
logo_path: "/assets/scrappers/igdb.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.IGDB_API_ENABLED,
|
||||
},
|
||||
{
|
||||
name: "MobyGames",
|
||||
value: "moby",
|
||||
logo_path: "/assets/scrappers/moby.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.MOBY_API_ENABLED,
|
||||
},
|
||||
{
|
||||
name: "SteamgridDB",
|
||||
value: "sgdb",
|
||||
logo_path: "/assets/scrappers/sgdb.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.STEAMGRIDDB_ENABLED,
|
||||
},
|
||||
]);
|
||||
const defaultAdminUser = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
role: "admin",
|
||||
});
|
||||
const step = ref(1);
|
||||
const filledAdminUser = computed(
|
||||
() =>
|
||||
defaultAdminUser.value.username != "" &&
|
||||
defaultAdminUser.value.password != "",
|
||||
);
|
||||
const isFirstStep = computed(() => step.value == 1);
|
||||
const isLastStep = computed(() => step.value == 2);
|
||||
|
||||
// Functions
|
||||
async function finishWizard() {
|
||||
await userApi
|
||||
.createUser(defaultAdminUser.value)
|
||||
.then(async () => {
|
||||
await refetchCSRFToken();
|
||||
router.push({ name: "login" });
|
||||
})
|
||||
.catch(({ response, message }) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Unable to create user: ${
|
||||
response?.data?.detail || response?.statusText || message
|
||||
}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span id="bg" />
|
||||
|
||||
<v-container class="fill-height justify-center">
|
||||
<v-card class="translucent-dark px-3" elevation="0">
|
||||
<v-img src="/assets/isotipo.svg" class="mx-auto mt-4" width="70" />
|
||||
<v-stepper
|
||||
:mobile="smAndDown"
|
||||
class="bg-transparent"
|
||||
:width="xs ? '' : smAndDown ? 400 : 700"
|
||||
max-width="700"
|
||||
v-model="step"
|
||||
flat
|
||||
>
|
||||
<template v-slot:default="{ prev, next }">
|
||||
<v-stepper-header>
|
||||
<v-stepper-item
|
||||
class="text-white text-shadow"
|
||||
title="Create an admin user"
|
||||
:value="1"
|
||||
></v-stepper-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-stepper-item
|
||||
class="text-white text-shadow"
|
||||
title="Check metadata sources"
|
||||
:value="2"
|
||||
></v-stepper-item>
|
||||
|
||||
<!-- <v-divider></v-divider> -->
|
||||
|
||||
<!-- <v-stepper-item title="Finish" :value="3"></v-stepper-item> -->
|
||||
</v-stepper-header>
|
||||
|
||||
<v-stepper-window>
|
||||
<v-stepper-window-item :value="1" :key="1">
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-row v-if="smAndDown" no-gutters class="text-center mb-6">
|
||||
<v-col>
|
||||
<span>Create an admin user</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="text-white justify-center mt-3" no-gutters>
|
||||
<v-col cols="10" md="8">
|
||||
<v-form @submit.prevent>
|
||||
<v-text-field
|
||||
v-model="defaultAdminUser.username"
|
||||
required
|
||||
prepend-inner-icon="mdi-account"
|
||||
type="text"
|
||||
label="Username"
|
||||
variant="underlined"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="defaultAdminUser.password"
|
||||
required
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:type="visiblePassword ? 'text' : 'password'"
|
||||
label="Password"
|
||||
variant="underlined"
|
||||
:append-inner-icon="
|
||||
visiblePassword ? 'mdi-eye-off' : 'mdi-eye'
|
||||
"
|
||||
@click:append-inner="
|
||||
visiblePassword = !visiblePassword
|
||||
"
|
||||
/>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-stepper-window-item>
|
||||
|
||||
<v-stepper-window-item :value="2" :key="2">
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-row v-if="smAndDown" no-gutters class="text-center mb-6">
|
||||
<v-col>
|
||||
<span>Check metadata sources</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="justify-center align-center" no-gutters>
|
||||
<v-col id="sources">
|
||||
<v-list-item
|
||||
v-for="source in metadataOptions"
|
||||
class="text-white text-shadow"
|
||||
:title="source.name"
|
||||
:subtitle="
|
||||
source.disabled ? 'API key missing or invalid' : ''
|
||||
"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar size="30" rounded="1">
|
||||
<v-img :src="source.logo_path" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
<template #append>
|
||||
<span class="ml-2" v-if="source.disabled">❌</span>
|
||||
<span class="ml-2" v-else>✅</span>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-stepper-window-item>
|
||||
|
||||
<!-- <v-stepper-window-item :value="3" :key="3">
|
||||
<v-row class="text-center" no-gutters>
|
||||
<v-col>
|
||||
<span>Finished!</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-stepper-window-item> -->
|
||||
</v-stepper-window>
|
||||
|
||||
<v-stepper-actions :disabled="!filledAdminUser">
|
||||
<template #prev>
|
||||
<v-btn
|
||||
class="text-white text-shadow"
|
||||
:ripple="false"
|
||||
:disabled="isFirstStep"
|
||||
@click="prev"
|
||||
>{{ isFirstStep ? "" : "previous" }}</v-btn
|
||||
>
|
||||
</template>
|
||||
<template #next>
|
||||
<v-btn
|
||||
class="text-white text-shadow"
|
||||
@click="!isLastStep ? next() : finishWizard()"
|
||||
>{{ !isLastStep ? "Next" : "Finish" }}</v-btn
|
||||
>
|
||||
</template>
|
||||
</v-stepper-actions>
|
||||
</template>
|
||||
</v-stepper>
|
||||
</v-card>
|
||||
|
||||
<div id="version" class="position-absolute">
|
||||
<span class="text-white">{{ heartbeat.value.VERSION }}</span>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#bg {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: url("/assets/login_bg.png") center center;
|
||||
background-size: cover;
|
||||
}
|
||||
#sources {
|
||||
max-width: 300px;
|
||||
}
|
||||
#version {
|
||||
text-shadow:
|
||||
1px 1px 1px #000000,
|
||||
0 0 1px #000000;
|
||||
bottom: 0.3rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user