First commit

This commit is contained in:
Fred Boniface
2025-01-24 16:01:59 +00:00
commit eccc7e5a07
55 changed files with 4565 additions and 0 deletions

13
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents; width: 100%">%sveltekit.body%</div>
</body>
</html>

14
src/auth.ts Normal file
View File

@@ -0,0 +1,14 @@
import client from "$lib/db"
import { MongoDBAdapter } from "@auth/mongodb-adapter"
import { SvelteKitAuth } from "@auth/sveltekit"
import Keycloak from "@auth/sveltekit/providers/keycloak"
export const { handle, signIn, signOut } = SvelteKitAuth({
providers: [Keycloak],
trustHost: true,
adapter: MongoDBAdapter(client),
session: {
maxAge: 14400, // Limit session length to four hours
updateAge: 1800, // Update token every 30 minutes
},
})

1
src/hooks.server.ts Normal file
View File

@@ -0,0 +1 @@
export { handle } from "./auth"

68
src/lib/HeaderBar.svelte Normal file
View File

@@ -0,0 +1,68 @@
<script lang="ts">
import { page } from "$app/stores";
</script>
<div id="header-bar">
<div id="logo-box">
<a href="/">
<img class="logo-img" src="/logo/logo-colour.svg" alt="FJLA Logo" width="50" height="50">
<!--<img class="logo-img" id="logo-black" src="/logo/logo-black.svg" alt="FJLA Logo" width="50" height="50">
--> </a>
</div>
{#if $page.data.session}
<a class="account-link" href="/logout">Sign out</a>
{/if}
</div>
<div id="spacer"></div>
<style>
#header-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: darkslategrey;
height: 60px;
}
#logo-box {
padding: 5px;
height: 50px;
}
.logo-img {
position: absolute;
left: 5px;
}
.account-link {
position: absolute;
right: 10px;
top: 0px;
height: 60px;
width: 140px;
color: white;
background-color: darkslategrey;
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
font-weight: 900;
font-size: larger;
text-decoration: none;
text-align: center;
border-radius: 0px;
/* Flexbox styles for centering */
display: flex;
align-items: center;
justify-content: center;
}
.account-link:hover {
background-color: rgb(54, 90, 90);
}
#spacer {
height: 90px;
}
</style>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import MasterAppCard from "./MasterAppCard.svelte";
</script>
<div id="card-container">
<MasterAppCard appName={"My Account"} appDesc={"Manage your password and passkeys"} iconName={"myaccount"} sso={true} appUrl={"https://sso.fjla.uk/realms/FJLA.net/account"} />
<MasterAppCard appName={"Nextcloud"} appDesc={"Files, Email, Calendar, Chat"} iconName={"nextcloud"} sso={false} appUrl={"https://cloud.fjla.uk"} bgColor={"#0082c9"} />
<MasterAppCard appName={"Portainer"} appDesc={"Manage containerised workloads in Docker"} iconName={"portainer"} sso={true} appUrl={"https://swarm_nodes.fjla.net:9443"} nointernet={true} bgColor={"#3BBCED"} />
<MasterAppCard appName={"SpeedyF"} appDesc={"Online Compressor for PDF Files"} iconName={"speedyf"} sso={false} appUrl={"https://speedyf.fjla.uk"} bgColor={"#00001a"} />
<MasterAppCard appName={"Jellyfin"} appDesc={"Stream films and TV, watch Live TV"} iconName={"jellyfin"} sso={false} appUrl={"http://jf.fjla.net:8096"} nointernet={true} bgColor={"#AA5CC3"} />
<MasterAppCard appName={"Proxmox"} appDesc={"Manage virtual machines"} iconName={"proxmoxve"} sso={true} appUrl={"https://pve0124.fjla.net:8006"} nointernet={true} bgColor={"#e57000"} />
<MasterAppCard appName={"Home Assistant"} appDesc={"Smart Home Management"} iconName={"homeassistant"} sso={false} appUrl={"https://ha.fjla.uk"} bgColor={"#18BCF2"} />
<MasterAppCard appName={"Gitea"} appDesc={"Code & Package Repo"} iconName={"gitea"} sso={true} appUrl={"https://git.fjla.uk"} bgColor={"#609926"} />
</div>
<style>
#card-container {
width: 90%;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1px;
padding: 0px;
box-sizing: border-box;
}
</style>

View File

View File

@@ -0,0 +1,79 @@
<script lang="ts">
export let appName: string;
export let appDesc: string;
export let iconName: string;
export let sso: boolean;
export let nointernet: boolean = false;
export let appUrl: string = "#";
export let bgColor: string = "rgb(134, 134, 134)"
</script>
<div id="app-card" style="background-color:{bgColor}">
<a class="app-link" href={appUrl}>
<img src="/icons/{iconName}.svg" alt="{iconName}">
<div id="card-text">
<header id="app-name">{appName}</header>
<p id="app-desc">{appDesc}</p>
</div>
<div id="sign-on-type">
{#if sso}
<img src="/logo/logo-black.svg" alt="Single Sign-on" width=25 height=25>
{:else}
<img src="/icons/not-sso.svg" alt="Single Sign-on not supported" width=25 height=25>
{/if}
{#if nointernet}
<img src="/icons/nointernet.svg" alt="Not available when not connected to FJLA WiFi" width=25 height=25>
{/if}
</div>
</a>
</div>
<style>
#app-card {
width: calc(25% - 2px);
box-sizing: border-box;
text-align: center;
margin: auto;
margin-top: 0px;
margin-bottom: 0px;
text-decoration: none;
color: white;
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
padding: 10px;
border-radius: 3px;
transition: all 0.2s;
}
/* Responsive styles for smaller screens */
@media (max-width: 875px) {
#app-card {
width: calc(50% - 2px); /* 2 cards per row on smaller screens */
}
}
@media (max-width: 676px) {
#app-card {
width: calc(100% - 2px); /* 1 card per row on very small screens */
}
}
#app-card:hover {
opacity: 75%;
}
#app-name {
font-weight: 600;
font-size: larger;
}
#app-desc {
margin-top: 10px;
margin-bottom: 4px;
}
.app-link{
text-decoration: none;
color: white;
}
</style>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import { SignIn } from "@auth/sveltekit/components";
</script>
<SignIn provider="keycloak" redirectTo="/">
<div slot="submitButton" class="buttonPrimary">Login</div>
</SignIn>
<style>
</style>

35
src/lib/db.ts Normal file
View File

@@ -0,0 +1,35 @@
import { MongoClient, ServerApiVersion } from "mongodb";
if (!process.env.MONGODB_URI) {
console.log(process.env.MONGODB_URI)
console.log("MONGODB_URI Not valid, auth will not work");
}
const uri = process.env.MONGODB_URI || "mongodb://localhost:27017";
const options = {
serverApi: {
version: ServerApiVersion.v1,
strict: true,
deprecationErrors: true,
},
};
let client: MongoClient
if (process.env.NODE_ENV !== "production") {
const globalWithMongo = global as typeof globalThis & {
_mongoClient?: MongoClient
}
if (!globalWithMongo._mongoClient) {
globalWithMongo._mongoClient = new MongoClient(uri, options)
}
client = globalWithMongo._mongoClient
} else {
// In production mode, it's best to not use a global variable.
client = new MongoClient(uri, options)
}
// Export a module-scoped MongoClient. By doing this in a
// separate module, the client can be shared across functions.
export default client

1
src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import Login from "./buttons/Login.svelte";
</script>
<div id="content-container">
<img id="large-logo" src="/logo/logo-colour.svg" width=125 height=125 alt="Logo">
<div id="bounding-box">
<header>You are not signed in</header>
<Login />
<p>When you login for the first time, you will have to verify your email address and register a passkey to enable your account, <a href="passkey">click here</a> for help.</p>
<p><a href="https://sso.fjla.uk/realms/FJLA.net/login-actions/reset-credentials" target="_blank">Forgot your password?</a></p>
</div>
</div>
<style>
#content-container {
width: 100%;
text-align: center;
margin: 10 auto;
padding: 25px;
box-sizing: border-box;
}
#large-logo {
margin: 0 auto;
}
#bounding-box {
margin: auto;
text-align: center;
background-color: lightgrey;
width: 75%;
min-width: 200px;
max-width: 800px;
border-radius: 25px;
margin-top: 25px;
}
header {
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
font-size: large;
padding: 15px;
}
p {
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
padding: 15px;
}
</style>

View File

@@ -0,0 +1,9 @@
import type { LayoutServerLoad } from "./$types"
export const load: LayoutServerLoad = async (event) => {
const session = await event.locals.auth()
return {
session,
}
}

View File

@@ -0,0 +1,4 @@
<script lang="ts">
</script>
<slot />

View File

@@ -0,0 +1,8 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const session = await event.locals.auth();
if (!session?.user) throw redirect(303, '/login');
return {};
};

37
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,37 @@
<script lang="ts">
import { page } from "$app/state";
import NotLoggedIn from "$lib/notLoggedIn.svelte";
import AppCardArray from "$lib/app-cards/AppCardArray.svelte";
import HeaderBar from "$lib/HeaderBar.svelte";
</script>
{#if page.data.session}
<HeaderBar />
<h1>{page?.data?.session?.user?.name}'s Apps</h1>
<div id="icon-key">
<img src="/logo/logo-black.svg" alt="FJLA Logo" width=20 height=20> Single Sign On
<br>
<img src="/icons/not-sso.svg" alt="Separate Password Icon" width=20 height=20 style="background-color:black;border-radius:5px;"> Uses a separate Login
<br>
<img src="/icons/nointernet.svg" alt="Only on FJLA WiFi" width=20 height=20 style="background-color:black;border-radius:5px;"> Only available on FJLA WiFi
</div>
<AppCardArray />
{:else}
<NotLoggedIn />
{/if}
<style>
h1 {
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
text-align: center;
margin: 5px;
margin-top: -10px;
}
#icon-key {
margin: auto;
text-align: center;
font-size: 16px;
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
margin-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,45 @@
const handler = async ({ request }: { request: Request }) => {
// Log the HTTP method
console.log('HTTP Method:', request.method);
// Log the full URL
console.log('URL:', request.url);
// Log request headers
console.log('Headers:', Object.fromEntries(request.headers.entries()));
// Log the body, depending on its content type
const contentType = request.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
const jsonBody = await request.json();
console.log('JSON Body:', JSON.stringify(jsonBody, null, 2));
} else if (contentType.includes('application/x-www-form-urlencoded')) {
const formData = await request.formData();
const formObject: Record<string, string> = {};
formData.forEach((value, key) => {
formObject[key] = value.toString();
});
console.log('Form Data:', formObject);
} else if (contentType.includes('multipart/form-data')) {
const formData = await request.formData();
const formObject: Record<string, string> = {};
formData.forEach((value, key) => {
formObject[key] = value.toString();
});
console.log('Multipart Form Data:', formObject);
} else {
const textBody = await request.text();
console.log('Text Body:', textBody);
}
return new Response('Request logged!', { status: 200 });
};
// Bind the handler to all HTTP methods
export const GET = handler;
export const POST = handler;
export const PUT = handler;
export const DELETE = handler;
export const PATCH = handler;
export const OPTIONS = handler;

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { onMount } from "svelte";
import { signOut } from "@auth/sveltekit/client";
const changePwLink: string = "https://sso.fjla.uk/realms/FJLA.net/account/account-security/signing-in"
onMount(async() => {
await signOut();
window.location.href = changePwLink;
})
</script>
<p>Redirecting, please wait...</p>
<p>If the page does not reload <a href={changePwLink}>click here</a>.</p>
<style>
p {
text-align: center;
margin: auto;
}
</style>

View File

@@ -0,0 +1,6 @@
<script lang="ts">
import { page } from "$app/state";
import NotLoggedIn from "$lib/notLoggedIn.svelte";
</script>
<NotLoggedIn />

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { onMount } from "svelte";
import { signOut } from "@auth/sveltekit/client";
const changePwLink: string = "https://sso.fjla.uk/realms/FJLA.net/login-actions/reset-credentials"
onMount(async() => {
await signOut();
window.location.href = changePwLink;
})
</script>
<p>Redirecting, please wait...</p>
<p>If the page does not reload <a href={changePwLink}>click here</a>.</p>
<style>
p {
text-align: center;
margin: auto;
}
</style>

View File

@@ -0,0 +1,8 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const session = await event.locals.auth();
if (session?.user) throw redirect(303, '/');
return {};
};

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import NotLoggedIn from "$lib/notLoggedIn.svelte";
</script>
<NotLoggedIn />

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { onMount } from "svelte";
import { signOut } from "@auth/sveltekit/client";
const keycloakUrl = "https://sso.fjla.uk";
const keycloakRealm = "FJLA.net";
const postLogoutRedirect = "https://fjla.uk/";
const clientId = "fjla-home"
const globalLogoutUrl = `${keycloakUrl}/realms/${keycloakRealm}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirect)}&client_id=${clientId}`;
onMount(async() => {
await signOut();
window.location.href = globalLogoutUrl;
})
</script>
<p>Signing out...</p>
<style>
p {
text-align: center;
margin: auto;
}
</style>

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import HeaderBar from "$lib/HeaderBar.svelte";
</script>
<HeaderBar />
<header>Passkey Help</header>
<p>FJLA requires a passkey registered to your account.</p>
<p>A passkey increases your password by supplementing it, or replacing it completely.</p>
<p class="subhead">What is a Passkey?</p>
<p>A passkey is a digital credential that allows users to authenticate and sign into their accounts without using a traditional password. It relies on biometric data such as a fingerprint, face scan, or PIN to verify the users identity, ensuring that the sign-in process is both secure and convenient. Passkeys are designed to be resistant to phishing attacks and other forms of cybercrime, as they cannot be stolen or shared like passwords. They are supported by various platforms and can be used across different devices to simplify account registration and improve the overall user experience.</p>
<p class="subhead">How do I get a Passkey?</p>
<p>Most mobile devices and many newer computers can act as a passkey themselves. Alternatively, you can purchase a USB passkey - <a href="https://www.yubico.com/gb/product/security-key-nfc-by-yubico-black/" target="_blank">example from Yubico.</a></p>
<p class="subhead">What's the point?</p>
<p>By configuring your accounts with a Passkey, security is quickly and easily improved massively. You can use passkeys on many websites and services - if you configure passwordless login, you don't even need to remember a password.</p>
<p class="subhead">How do I configure passwordless login?</p>
<p>Login to your FJLA Account on the homepage, then click on My Account, Click on the menu, then 'Account Security', then 'Signing In'. At the bottom of the page, add your passkey in the Passwordless section.</p>
<p>If you don't configure passwordless, you will need your password and your passkey to login. Some passkeys do not support passwordless login.</p>
<style>
header {
text-align: center;
font-size: larger;
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
font-weight: 600;
margin-bottom: 40px;
}
p {
text-align: center;
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
margin: auto;
width: 80%;
max-width: 600px;
}
.subhead {
margin-top: 40px;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,3 @@
import { signIn } from "../../auth";
import type { Actions } from "./$types";
export const actions: Actions = { default: signIn }