Split admin login into its own route and add auth guard to /admin
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 28s

- Add /admin/login route for Login.vue as a standalone page
- Add requiresAdmin guard to /admin route
- Update auth guard redirect to /admin/login with redirect query param
- Update nginx @auth_denied to redirect to /admin/login
- Remove Login component from Admin.vue; drop v-if auth checks (guard handles access)
- Remove stale view files from old views/ structure (moved in prior commit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 11:19:10 +01:00
parent 4d154ff837
commit 18b50f1ce6
12 changed files with 15 additions and 501 deletions

View File

@@ -246,7 +246,7 @@ http {
} }
location @auth_denied { location @auth_denied {
return 302 /admin; return 302 /admin/login;
} }
location /searxng { location /searxng {

View File

@@ -22,10 +22,16 @@ const router = createRouter({
name: "home", name: "home",
component: () => import("@/views/home/Home.vue"), component: () => import("@/views/home/Home.vue"),
}, },
{
path: "admin/login",
name: "admin-login",
component: () => import("@/views/admin/Login.vue"),
},
{ {
path: "admin", path: "admin",
name: "admin", name: "admin",
component: () => import("@/views/admin/Admin.vue"), component: () => import("@/views/admin/Admin.vue"),
meta: { requiresAdmin: true },
}, },
{ {
path: "bookmarks", path: "bookmarks",
@@ -94,7 +100,7 @@ router.beforeEach(async (to) => {
}); });
}); });
} }
if (!useAuthStore().user.admin) return "/admin"; if (!useAuthStore().user.admin) return { path: "/admin/login", query: { redirect: to.fullPath } };
}); });
export default router; export default router;

View File

@@ -1,13 +0,0 @@
<template>
<main class="flex flex-col items-center">
<div
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
>
<h1>404</h1>
<RouterLink to="/" class="bdr-2">
<img src="/img/memes/epic.jpeg" loading="lazy" />
</RouterLink>
<h1>Click her, she will take you home</h1>
</div>
</main>
</template>

View File

@@ -1,255 +0,0 @@
<script setup>
import LinkTable from "@/components/util/LinkTable.vue";
const links = [
[
"Reading Links",
[
{
name: "Substack",
link: "https://substack.com/",
},
{
name: "Medium",
link: "https://medium.com/",
},
{
name: "4Chan",
link: "https://www.4chan.org/",
},
],
],
[
"Job Links",
[
{
name: "LinkedIn",
link: "https://www.linkedin.com/",
},
{
name: "Jack and Jill",
link: "https://app.jackandjill.ai",
},
{
name: "LinkedIn",
link: "https://www.linkedin.com/",
},
{
name: "Prospects",
link: "https://www.prospects.ac.uk/",
},
{
name: "GOV",
link: "https://findajob.dwp.gov.uk",
},
{
name: "Glassdoor",
link: "https://www.glassdoor.co.uk/",
},
{
name: "Indeed",
link: "https://www.indeed.co.uk/",
},
],
],
[
"Learning Links",
[
{
name: "Leetcode",
link: "https://leetcode.com/",
},
{
name: "ISLP",
link: "https://hastie.su.domains/ISLP/ISLP_website.pdf.download.html",
},
],
],
[
"Social Links",
[
{
name: "Outlook",
link: "https://outlook.live.com/",
},
{
name: "Gmail",
link: "https://mail.google.com/",
},
{
name: "Whatsapp",
link: "https://web.whatsapp.com/",
},
],
],
[
"Radio links",
[
{
name: "Radio Helsinki",
link: "https://www.radiohelsinki.fi/",
},
{
name: "Palanga Street Radio",
link: "https://palanga.live/",
},
{
name: "IDA Radio",
link: "https://idaidaida.net/",
},
{
name: "Tīrkultūra",
link: "https://www.tirkultura.lv/",
},
],
],
[
"Hacking Links",
[
{
name: "pwn.college",
link: "https://pwn.college/",
},
{
name: "OSINT Framework",
link: "https://osintframework.com/",
},
{
name: "OverTheWire",
link: "https://overthewire.org/",
},
{
name: "TryHackMe",
link: "https://tryhackme.com/",
},
],
],
[
"Chinese Links",
[
{
name: "MDBG Chinese Dictionary",
link: "https://www.mdbg.net/chinese/dictionary",
},
{
name: "Stroke Order",
link: "https://www.strokeorder.com/",
},
{
name: "HSK 1 Peking University",
link: "https://youtube.com/playlist?list=PLVWfp7qXLmKVfSUkucXErLncKn-JqgBbK&si=2ytO3inS8-iOAOx2",
},
{
name: "Stroke Order",
link: "https://www.strokeorder.com/",
},
{
name: "Offbeat Mandarin",
link: "https://www.youtube.com/@OffbeatMandarin",
},
],
],
[
"Art links",
[
{
name: "Frida Kahlo",
link: "https://www.fridakahlo.org/",
},
{
name: "Cameron's World",
link: "https://www.cameronsworld.net/",
},
{
name: "Neocities",
link: "https://neocities.org/",
},
],
],
[
"Vue links",
[
{
name: "Vue",
link: "https://vuejs.org/guide/introduction.html",
},
{
name: "Vue Router",
link: "https://router.vuejs.org/introduction.html",
},
{
name: "Pinia",
link: "https://pinia.vuejs.org/introduction.html",
},
],
],
[
"Go links",
[
{
name: "Golang",
link: "https://golang.org/doc/",
},
{
name: "Gin Gonic",
link: "https://gin-gonic.com/en/docs/introduction/",
},
{
name: "GORM",
link: "https://gorm.io/gen/index.html",
},
],
],
[
"Doc links",
[
{
name: "Rust",
link: "https://doc.rust-lang.org/stable/book/index.html",
},
{
name: "Javascript",
link: "https://developer.mozilla.org/en-US/docs/Web/JavaScript",
},
{
name: "Python",
link: "https://docs.python.org/3/",
},
],
],
[
"Article links",
[
{
name: "Go and GORM",
link: "https://medium.com/@chaewonkong/learn-go-understanding-and-implementing-foreign-keys-with-gorm-6d7608e1dbf6",
},
{
name: "JWT Auth in GO",
link: "https://medium.com/monstar-lab-bangladesh-engineering/jwt-auth-in-go-dde432440924",
},
{
name: "Websockets in GO",
link: "https://medium.com/@tanngontn/golang-gin-framework-with-normal-websocket-and-websocket-with-producer-is-rabbitmq-guide-93cad7d290f7",
},
],
],
];
</script>
<template>
<main class="items-center flex flex-col">
<div
class="a4page-portrait bdr-1 flex flex-row flex-wrap overflow-x-auto gap-1"
>
<div class="w-full h-fit">
<LinkTable
class="flex flex-col flex-wrap"
v-for="link in links"
:title="link[0]"
:items="link[1]"
/>
</div>
</div>
</main>
</template>

View File

@@ -1,56 +0,0 @@
<script setup>
import Link from "@/components/text/Link.vue";
import InlineLink from "@/components/text/InlineLink.vue";
import Header from "@/components/text/Header.vue";
import Paragraph from "@/components/text/Paragraph.vue";
const links = [
{ name: "GitHub", href: "https://github.com/SteveThePug" },
{ name: "Gitea", href: "/gitea/explore/repos" },
{ name: "Spotify", href: "https://open.spotify.com/user/stevethepug" },
];
</script>
<template>
<main class="flex justify-center px-4 py-16">
<div class="max-w-xl w-full flex flex-col gap-12">
<section>
<Header>Adam French</Header>
<Paragraph>
Junior software engineer focused on full-stack development,
systems programming, and infrastructure. First Class Honours
in Computer Science with Mathematics from Leeds and
Waterloo.
</Paragraph>
</section>
<section>
<Header>About</Header>
<Paragraph>
This website is self-hosted and has a lot more on it than it
needs to. Please have a look at my
<InlineLink to="/cv">CV</InlineLink> for a full breakdown of
my experience, projects, and skills. Please visit
<InlineLink to="/stp">STP</InlineLink> for the prefered but
less professional experience.
</Paragraph>
</section>
<nav class="navRow flex flex-row flex-wrap gap-4 justify-around">
<Link to="/cv"> CV </Link>
<Link to="/stp"> STP </Link>
<Link href="mailto:adam.a.french@outlook.com"> Email </Link>
<Link v-for="link in links" :key="link.name" :href="link.href">
{{ link.name }}
</Link>
</nav>
</div>
</main>
</template>
<style scoped>
.navRow > a {
padding: 0.5rem 1rem;
border: 1px solid currentColor;
}
</style>

View File

@@ -1,75 +0,0 @@
<script setup>
import Markdown from "@/components/util/Markdown.vue";
import { ref, onMounted } from "vue";
import axios from "axios";
import { useRoute } from "vue-router";
const file = ref(null);
const filename = ref("");
const last_edited = ref(null);
// if the address is https://www.adam-french.co.uk/notes/PATH
// request from https://www.adam-french.co.uk/api/notes/PATH
const route = useRoute();
const pathArray = route.params.path;
const path = Array.isArray(pathArray) ? pathArray.join("/") : pathArray;
const url = `/api/notes/${path}`;
function getFilename(headers) {
const disposition = headers["content-disposition"];
if (!disposition) return null;
const match = disposition.match(/filename="?([^"]+)"?/);
return match ? match[1] : null;
}
async function fetchFile() {
const response = await axios.get(url, { responseType: "blob" });
filename.value = getFilename(response.headers);
const lastModified = response.headers["last-modified"];
last_edited.value = lastModified ? new Date(lastModified) : null;
if (filename.value.toLowerCase().endsWith(".md")) {
const text = await response.data.text();
file.value = fixLinks(text);
} else {
file.value = response.data;
}
}
function fixLinks(filedata) {
return filedata.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
if (
url.startsWith("http://") ||
url.startsWith("https://") ||
url.startsWith("#") ||
url.startsWith("./") ||
url.startsWith("../") ||
url.startsWith("//")
) {
return match;
}
return `[${text}](/notes/${url})`;
});
}
onMounted(fetchFile);
</script>
<template>
<main class="items-center flex flex-col">
<div class="background" />
<div
v-if="file"
class="a4page-portrait border-primary-1 flex flex-col relative overflow-scroll gap-1 bg-bg_primary"
>
<h1>{{ filename }}</h1>
<small>{{ last_edited }}</small>
<Markdown class="flex-1 border-box text-wrap" :source="file" />
</div>
<div v-else>Loading</div>
</main>
</template>

View File

@@ -1,20 +0,0 @@
<script setup>
import RouterTable from "@/components/util/RouterTable.vue";
const shrine_links = [
{ name: "Demoman", link: "/shrines/demoman" },
{ name: "Evangelion", link: "/shrines/evangelion" },
{ name: "GTO", link: "/shrines/gto" },
{ name: "Skipskipbenben", link: "/shrines/skipskipbenben" },
];
</script>
<template>
<main class="items-center flex flex-col">
<div class="background" />
<div
class="a4page-portrait bdr-1 flex flex-col relative overflow-scroll gap-1"
>
<RouterTable :linkArr="shrine_links" />
</div>
</main>
</template>

View File

@@ -1,8 +1,4 @@
<script setup> <script setup>
import { ref } from "vue";
import { useAuthStore } from "@/stores/auth";
import Login from "./Login.vue";
import CreateUser from "./CreateUser.vue"; import CreateUser from "./CreateUser.vue";
import CreatePost from "./CreatePost.vue"; import CreatePost from "./CreatePost.vue";
import CreateFavorite from "./CreateFavorite.vue"; import CreateFavorite from "./CreateFavorite.vue";
@@ -10,21 +6,18 @@ import CreateActivity from "./CreateActivity.vue";
import CreateRowing from "./CreateRowing.vue"; import CreateRowing from "./CreateRowing.vue";
import ManageUsers from "./ManageUsers.vue"; import ManageUsers from "./ManageUsers.vue";
import ManageRadio from "./ManageRadio.vue"; import ManageRadio from "./ManageRadio.vue";
const auth = useAuthStore();
</script> </script>
<template> <template>
<main class="justify-center flex flex-row w-full h-full"> <main class="justify-center flex flex-row w-full h-full">
<div class="bdr-1 flex flex-col"> <div class="bdr-1 flex flex-col">
<Login class="bdr-2 bg-bg_primary" /> <CreateUser class="bdr-2 bg-bg_primary" />
<CreateUser class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" /> <CreatePost class="bdr-2 bg-bg_primary" />
<CreatePost class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" /> <CreateFavorite class="bdr-2 bg-bg_primary" />
<CreateFavorite class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" /> <CreateActivity class="bdr-2 bg-bg_primary" />
<CreateActivity class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" /> <CreateRowing class="bdr-2 bg-bg_primary" />
<CreateRowing class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" /> <ManageUsers class="bdr-2 bg-bg_primary" />
<ManageUsers class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" /> <ManageRadio class="bdr-2 bg-bg_primary" />
<ManageRadio class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
</div> </div>
</main> </main>
</template> </template>

View File

@@ -1,29 +0,0 @@
<script setup>
import VideoTable from "@/components/util/VideoTable.vue";
import Link from "@/components/text/Link.vue";
const videoSources = [
{ name: "demoman", link: "/img/demoman/1760582395316219.webm" },
{ name: "demoman", link: "/img/demoman/1761052136609718.webm" },
{ name: "demoman", link: "/img/demoman/1761088452011210.mp4" },
{ name: "demoman", link: "/img/demoman/1761570214170465.webm" },
{ name: "demoman", link: "/img/demoman/1761828457509465.webm" },
];
</script>
<template>
<main class="items-center flex flex-col">
<div
class="a4page-portrait bdr-1 flex flex-row relative overflow-scroll items-center"
>
<p>
<Link href="https://wiki.teamfortress.com/wiki/Demoman"
>The goat</Link
>
</p>
<div>
<VideoTable :sourceArr="videoSources" />
</div>
</div>
</main>
</template>

View File

@@ -1,13 +0,0 @@
<script setup>
import Wip from "@/components/util/Wip.vue";
</script>
<template>
<main class="items-center flex flex-col">
<div
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
>
<Wip />
</div>
</main>
</template>

View File

@@ -1,11 +0,0 @@
<script setup>
import Wip from "@/components/util/Wip.vue";
</script>
<template>
<main class="items-center flex flex-col">
<div class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll">
<Wip />
</div>
</main>
</template>

View File

@@ -1,13 +0,0 @@
<script setup>
import Wip from "@/components/util/Wip.vue";
</script>
<template>
<main class="items-center flex flex-col">
<div
class="a4page-portrait items-center bdr-1 flex flex-col relative overflow-scroll"
>
<Wip />
</div>
</main>
</template>