refactor: update routing to use 'home' instead of 'dashboard' and place login and setup under Auth layout

This commit is contained in:
zurdi
2024-12-04 20:03:57 +00:00
parent e936449aaf
commit 23a23bb0b8
23 changed files with 377 additions and 412 deletions

View File

@@ -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]

View File

@@ -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);

View File

@@ -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();
});

View File

@@ -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);

View 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>

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -32,7 +32,7 @@ export default defineStore("navigation", {
},
goHome() {
this.resetDrawers();
this.$router.push({ name: "dashboard" });
this.$router.push({ name: "home" });
},
goScan() {
this.resetDrawers();

View 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>

View 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>

View File

@@ -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";

View File

@@ -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>

View File

@@ -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";

View File

@@ -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>