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

24
.dockerignore Normal file
View File

@ -0,0 +1,24 @@
node_modules
Dockerfile
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

15
.prettierrc Normal file
View File

@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

15
Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM node:22-slim as builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . ./
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
ENV NODE_ENV=production
COPY --from=builder /app/build ./
EXPOSE 3000
CMD ["node", "index.js"]

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

34
eslint.config.js Normal file
View File

@ -0,0 +1,34 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
}
);

3715
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "fjla-home",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.3",
"@eslint/js": "^9.17.0",
"@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/node": "^22.10.7",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.11"
},
"dependencies": {
"@auth/mongodb-adapter": "^3.7.4",
"@auth/sveltekit": "^1.7.4",
"mongodb": "^6.12.0"
}
}

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 }

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

1
static/icons/gitea.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#fff" fill-rule="evenodd" d="M15.46 3.206s.14-.003.245.102l.01.01c.064.06.258.244.285 1.074c0 2.902-1.405 5.882-1.405 5.882a9 9 0 0 1-.359.713c-.458.802-.786 1.153-.786 1.153s-.318.379-.675.595c-.415.265-.72.263-.72.263L7.247 13c-.636-.079-1.29-.736-1.927-1.578c-.47-.677-.779-1.413-.779-1.413s-2.51.034-3.675-1.394C.235 7.895.103 7.067.06 6.769q0-.012-.004-.029c-.05-.324-.285-1.873.821-2.86c.517-.496 1.148-.638 1.37-.684c.371-.081.667-.06.903-.044l.09.006c.391.035 3.99.216 3.99.216s1.532.066 2.27.056c0 0 .003 1.853.003 2.78q.105.048.211.1l.212.1V3.427q.494-.005.996-.017h.011c1.545-.036 4.528-.204 4.528-.204ZM2.113 8.026s.28.26.94.477c.43.152 1.094.231 1.094.231S3.699 7.5 3.516 6.757c-.22-.886-.4-2.398-.4-2.398s-.438-.015-.789.079c-.766.19-.98.763-.98.763s-.384.688.036 1.813c.244.672.73 1.013.73 1.013Zm8.084 3.607c.344-.023.499-.392.499-.392s1.24-2.486 1.4-2.878a.7.7 0 0 0 .046-.438c-.07-.267-.39-.412-.39-.412l-1.926-.935l-.165.339l-.18.369a.46.46 0 0 1 .128.341s.433.186.743.387c0 0 .257.135.32.425c.075.273-.04.488-.066.539l-.002.003s-.216.51-.343.774l-.004.007q-.07.144-.139.28a.454.454 0 1 1-.32-.15s.41-.84.468-1.033c0 0 .096-.24.048-.38a.47.47 0 0 0-.19-.188a6 6 0 0 0-.678-.34s-.076.068-.18.09a.5.5 0 0 1-.158.014l-.611 1.25a.46.46 0 0 1 .046.587a.46.46 0 0 1-.578.138a.46.46 0 0 1-.232-.51a.46.46 0 0 1 .44-.35L8.8 7.886a.457.457 0 0 1 .361-.744l.185-.375l.167-.341l-.579-.281s-.251-.125-.458-.072a.6.6 0 0 0-.114.039c-.189.084-.31.33-.31.33L6.668 9.293s-.124.254-.068.46c.048.252.325.397.325.397l2.874 1.4l.135.054s.114.04.262.03Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

1
static/icons/gotify.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="M32.632 35.406c-1.59 2.423-3.29 5.035-7.064 6.175c-3.782 1.13-9.758.967-13.407-.719c-3.658-1.695-4.99-4.922-4.31-7.422c.67-2.508 3.343-4.3 4.55-6.224c1.197-1.935.929-4.003.613-5.918a28 28 0 0 1-.613-5.095a10.3 10.3 0 0 1 1.029-3.737"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="M14.555 12.612c-6.943-.48-1.436-6.704.958-1.916"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="M15.181 10.13c4.55-5.266 21.4-6.74 23.076 6.666"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="M19.822 7.273c.185-1.999 1.217-2.154 2.616-.672m15.819 10.196a1.214 1.214 0 0 1 1.312 1.082a1.345 1.345 0 0 1-2.633 0a1.217 1.217 0 0 1 1.321-1.082"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="M37.06 18.358c-5.507 3.954 5.381 4.123 2.509-.386M28.68 11.175a5.746 5.746 0 1 1-5.745 5.746a5.744 5.744 0 0 1 5.746-5.746"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="M30.596 15.724a1.323 1.323 0 1 1-1.197 1.312a1.257 1.257 0 0 1 1.197-1.312m7.901 5.424v1.785M36.29 11.294c2.227-.738 4.476 2.871 3.03 5.951m-12.392 8.209l13.14-2.864a1.4 1.4 0 0 1 1.664 1.067l.001.006l1.734 7.948a1.4 1.4 0 0 1-1.063 1.666l-13.149 2.864a1.4 1.4 0 0 1-1.665-1.066l-.001-.006l-1.733-7.949a1.4 1.4 0 0 1 1.066-1.665ZM8.884 31.378c-5.77-2.452-1.072-5.023.829-1.03M4.5 37.99q3.112 2.872 5.028-.719m21.454.497c1.676-.389 2.225.73 1.53 3.334"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="M23.174 26.498q1.197 2.288 3.83.134"/><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" d="m26.047 26.976l9.098 3.965l6.294-7.882M28.479 36.09l4.74-5.988m3.341-.932l6.691 3.508"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M22.939 10.627L13.061.749a1.505 1.505 0 0 0-2.121 0l-9.879 9.878C.478 11.21 0 12.363 0 13.187v9c0 .826.675 1.5 1.5 1.5h9.227l-4.063-4.062a2 2 0 0 1-.664.113c-1.13 0-2.05-.92-2.05-2.05s.92-2.05 2.05-2.05s2.05.92 2.05 2.05c0 .233-.041.456-.113.665l3.163 3.163V9.928a2.05 2.05 0 0 1-1.15-1.84c0-1.13.92-2.05 2.05-2.05s2.05.92 2.05 2.05a2.05 2.05 0 0 1-1.15 1.84v8.127l3.146-3.146A2.05 2.05 0 0 1 18 12.239c1.13 0 2.05.92 2.05 2.05s-.92 2.05-2.05 2.05a2 2 0 0 1-.709-.13L12.9 20.602v3.088h9.6c.825 0 1.5-.675 1.5-1.5v-9c0-.825-.477-1.977-1.061-2.561z"/></svg>

After

Width:  |  Height:  |  Size: 659 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M12 .002C8.826.002-1.398 18.537.16 21.666c1.56 3.129 22.14 3.094 23.682 0S15.177 0 12 0zm7.76 18.949c-1.008 2.028-14.493 2.05-15.514 0C3.224 16.9 9.92 4.755 12.003 4.755c2.081 0 8.77 12.166 7.759 14.196zM12 9.198c-1.054 0-4.446 6.15-3.93 7.189c.518 1.04 7.348 1.027 7.86 0c.511-1.027-2.874-7.19-3.93-7.19z"/></svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="#fff" stroke-dasharray="28" stroke-dashoffset="28" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4 21v-1c0 -3.31 2.69 -6 6 -6h4c3.31 0 6 2.69 6 6v1"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.4s" values="28;0"/></path><path d="M12 11c-2.21 0 -4 -1.79 -4 -4c0 -2.21 1.79 -4 4 -4c2.21 0 4 1.79 4 4c0 2.21 -1.79 4 -4 4Z"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.4s" dur="0.4s" values="28;0"/></path></g></svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M12.018 6.537c-2.5 0-4.6 1.712-5.241 4.015c-.56-1.232-1.793-2.105-3.225-2.105A3.57 3.57 0 0 0 0 12a3.57 3.57 0 0 0 3.552 3.553c1.432 0 2.664-.874 3.224-2.106c.641 2.304 2.742 4.016 5.242 4.016c2.487 0 4.576-1.693 5.231-3.977c.569 1.21 1.783 2.067 3.198 2.067A3.57 3.57 0 0 0 24 12a3.57 3.57 0 0 0-3.553-3.553c-1.416 0-2.63.858-3.199 2.067c-.654-2.284-2.743-3.978-5.23-3.977m0 2.085c1.878 0 3.378 1.5 3.378 3.378s-1.5 3.378-3.378 3.378A3.36 3.36 0 0 1 8.641 12c0-1.878 1.5-3.378 3.377-3.378m-8.466 1.91c.822 0 1.467.645 1.467 1.468s-.644 1.467-1.467 1.468A1.45 1.45 0 0 1 2.085 12a1.45 1.45 0 0 1 1.467-1.467zm16.895 0c.823 0 1.468.645 1.468 1.468s-.645 1.468-1.468 1.468A1.45 1.45 0 0 1 18.98 12a1.45 1.45 0 0 1 1.467-1.467z"/></svg>

After

Width:  |  Height:  |  Size: 837 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="#fff"><path stroke="#fff" d="M5 5a9.85 9.85 0 0 0-3 7.083C2 17.56 6.477 22 12 22a10 10 0 0 0 7-2.835"/><path stroke="#fff" d="M15.5 16c-.617 3.532-1.86 6-3.5 6c-2.21 0-4-4.477-4-10c0-1.231.089-2.41.252-3.5M2 12h10"/><path fill="#fff" d="M7.162 2.398a.75.75 0 0 0 .676 1.339zm13.1 13.764a.75.75 0 0 0 1.34.676zM16 12h-.75c0 .414.336.75.75.75zM8.303 4.726a.75.75 0 0 0 1.396.548zM12 2.75A9.25 9.25 0 0 1 21.25 12h1.5c0-5.937-4.813-10.75-10.75-10.75zm-4.162.987A9.2 9.2 0 0 1 12 2.75v-1.5a10.7 10.7 0 0 0-4.838 1.148zM21.25 12a9.2 9.2 0 0 1-.987 4.162l1.339.676A10.7 10.7 0 0 0 22.75 12zM12 2.75c.28 0 .622.139 1.012.551c.392.414.78 1.054 1.12 1.906c.68 1.701 1.118 4.103 1.118 6.793h1.5c0-2.832-.458-5.431-1.225-7.35c-.383-.957-.858-1.782-1.423-2.38c-.567-.599-1.277-1.02-2.102-1.02zM9.7 5.274c.335-.855.755-1.51 1.183-1.938c.433-.432.822-.586 1.117-.586v-1.5c-.824 0-1.572.42-2.177 1.025c-.61.61-1.13 1.455-1.52 2.451zM16 12.75h6v-1.5h-6z"/><path stroke="#fff" d="m2 2l20 20"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
static/icons/not-sso.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M2 19v-2h20v2zm1.15-6.05l-1.3-.75l.85-1.5H1V9.2h1.7l-.85-1.45L3.15 7L4 8.45L4.85 7l1.3.75L5.3 9.2H7v1.5H5.3l.85 1.5l-1.3.75l-.85-1.5zm8 0l-1.3-.75l.85-1.5H9V9.2h1.7l-.85-1.45l1.3-.75l.85 1.45l.85-1.45l1.3.75l-.85 1.45H15v1.5h-1.7l.85 1.5l-1.3.75l-.85-1.5zm8 0l-1.3-.75l.85-1.5H17V9.2h1.7l-.85-1.45l1.3-.75l.85 1.45l.85-1.45l1.3.75l-.85 1.45H23v1.5h-1.7l.85 1.5l-1.3.75l-.85-1.5z"/></svg>

After

Width:  |  Height:  |  Size: 491 B

1
static/icons/omada.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M12.766 7.75c-1.515 0-2.9.672-3.785 1.907l.658.675c.71-.99 1.908-1.625 3.127-1.625c1.22 0 2.421.634 3.131 1.625l.656-.674c-.885-1.235-2.271-1.907-3.787-1.907m-.006 2.192c-.966 0-1.842.4-2.48 1.194l.665.684c.461-.573 1.127-.921 1.815-.921s1.354.363 1.815.937l.674-.692a3.13 3.13 0 0 0-2.489-1.202m5.958.993v1.736a2 2 0 0 0-1.276-.466c-1.1 0-1.993.897-1.993 2s.894 2 1.993 2c.485 0 .93-.175 1.276-.465v.304h.718v-5.109Zm-12.524 1.27c-.883 0-1.6.72-1.6 1.606v2.234h.717V13.81c0-.488.396-.885.882-.885c.487 0 .884.397.884.885v2.234h.717V13.81c0-.488.396-.885.883-.885s.884.397.884.885v2.234h.717V13.81c0-.885-.718-1.606-1.6-1.606a1.59 1.59 0 0 0-1.242.595a1.6 1.6 0 0 0-1.242-.595m6.668 0c-1.1 0-1.994.897-1.994 2s.894 2 1.994 2c.465 0 .916-.166 1.275-.467v.306h.718v-1.84a2 2 0 0 0-1.993-1.999m9.145 0a1.999 1.999 0 0 0 0 4c.465 0 .916-.166 1.275-.467v.306H24v-1.84a2 2 0 0 0-1.993-1.999m-20.012.045a2 2 0 0 0 0 4a2 2 0 0 0 0-4m10.867.676a1.28 1.28 0 0 1 0 2.558a1.278 1.278 0 0 1 0-2.559m4.58 0a1.28 1.28 0 0 1 0 2.558a1.279 1.279 0 0 1 0-2.559m4.565 0a1.279 1.279 0 0 1 0 2.558a1.28 1.28 0 0 1 0-2.559m-19.999.058a1.279 1.279 0 0 1 0 2.558a1.279 1.279 0 0 1 0-2.558"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M12.504 0v1.023l-.01-.015l-6.106 3.526H3.417v.751h5.359v3.638h1.942V5.284h1.786V15.7c.027 0 .54-.01.751.091V5.285h.531v10.608c.293.147.55.312.751.54V5.286h6.046v-.75h-1.267l-6.061-3.5V0zm0 1.87v2.664H7.889zm.751.031l4.56 2.633h-4.56zM9.142 5.285h1.21v1.686h-1.21zm-4.736 2.73v1.951h1.942v-1.95zm2.19 0v1.951h1.941v-1.95zm-2.19 2.171v1.951h1.942v-1.95zm2.19 0v1.951h1.941v-1.95zm2.18 0v1.951h1.942v-1.95zM4.36 12.43a3.73 3.73 0 0 0-.494 1.851c0 1.227.604 2.308 1.52 2.986c.239-.064.477-.1.724-.11c.1 0 .165.01.266.019c.284-1.191 1.383-1.988 2.665-1.988c.724 0 1.438.201 1.924.668c.229-.476.302-1.007.302-1.575c0-.65-.165-1.292-.494-1.85zm4.828 3.16c-1.21 0-2.226.844-2.492 1.97a1 1 0 0 0-.275-.009a2.56 2.56 0 0 0-2.564 2.556a2.565 2.565 0 0 0 3.096 2.5A2.58 2.58 0 0 0 9.233 24c.862 0 1.622-.43 2.09-1.081a2.557 2.557 0 0 0 4.186-1.97c0-.567-.193-1.099-.504-1.52a2.557 2.557 0 0 0-3.866-2.94a2.57 2.57 0 0 0-1.951-.898z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M4.928 1.825c-1.09.553-1.09.64-.07 1.78c5.655 6.295 7.004 7.782 7.107 7.782c.139.017 7.971-8.542 8.058-8.801c.034-.07-.208-.312-.519-.536c-.415-.312-.864-.433-1.712-.467c-1.59-.104-2.144.242-4.115 2.455c-.899 1.003-1.66 1.833-1.66 1.833c-.017 0-.76-.813-1.642-1.798S8.473 2.1 8.127 1.91c-.796-.45-2.421-.484-3.2-.086zM1.297 4.367C.45 4.695 0 5.007 0 5.248c0 .121 1.331 1.678 2.94 3.459c1.625 1.78 2.939 3.268 2.939 3.302s-1.331 1.522-2.94 3.303C1.314 17.11.017 18.683.035 18.822c.086.467 1.504 1.055 2.541 1.055c1.678-.018 2.058-.312 5.603-4.202c1.78-1.954 3.233-3.614 3.233-3.666c0-.069-1.435-1.694-3.199-3.63c-2.3-2.508-3.423-3.632-3.96-3.874c-.812-.398-2.126-.467-2.956-.138m18.467.12c-.502.26-1.764 1.505-3.943 3.891c-1.763 1.937-3.199 3.562-3.199 3.631c0 .07 1.453 1.712 3.234 3.666c3.544 3.89 3.925 4.184 5.602 4.202c1.038 0 2.455-.588 2.542-1.055c.017-.156-1.28-1.712-2.905-3.493c-1.608-1.78-2.94-3.285-2.94-3.32s1.332-1.539 2.94-3.32C22.72 6.91 24.017 5.352 24 5.214c-.087-.45-1.366-.968-2.473-1.038c-.795-.034-1.21.035-1.763.312zM7.954 16.973c-2.144 2.369-3.908 4.374-3.943 4.46c-.034.07.208.312.52.537c.414.311.864.432 1.711.467c1.574.103 2.161-.26 4.15-2.508c.864-.968 1.608-1.78 1.625-1.78s.761.812 1.643 1.798c2.023 2.248 2.559 2.576 4.132 2.49c.848-.035 1.297-.156 1.712-.467c.311-.225.553-.467.519-.536c-.087-.26-7.92-8.819-8.058-8.801c-.069 0-1.867 1.954-4.011 4.34"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
static/icons/speedyf.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="#fff" fill-rule="evenodd" d="M5.8 14H5v1h.8c.3 0 .5-.2.5-.5s-.2-.5-.5-.5M11 2H3v16h13V7zM7.2 14.6c0 .8-.6 1.4-1.4 1.4H5v1H4v-4h1.8c.8 0 1.4.6 1.4 1.4zm4.1.5c0 1-.8 1.9-1.9 1.9H8v-4h1.4c1 0 1.9.8 1.9 1.9zM15 14h-2v1h1.5v1H13v1h-1v-4h3zm0-2H4V3h7v4h4zm-5.6 2H9v2h.4c.6 0 1-.4 1-1s-.5-1-1-1" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 411 B

1
static/icons/traccar.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" d="M6.001 1.61C.262 4.923-1.704 12.26 1.61 17.999s10.65 7.705 16.389 4.392S25.704 11.74 22.39 6S11.74-1.704 6 1.61m.706 1.222C11.77-.091 18.245 1.644 21.168 6.707s1.189 11.538-3.874 14.461s-11.538 1.189-14.462-3.874S1.644 5.756 6.707 2.832m3.914 14.315a3.768 3.768 0 1 1-3.768-6.526l1.884 3.263Zm5.725-11.395L15.17 7.247c.665.511 1.28 1.156 1.725 1.927s.696 1.626.807 2.458l1.882-.272a8.4 8.4 0 0 0-3.238-5.608m-2.272 3.011l-1.176 1.495c.274.216.548.48.734.8s.276.69.325 1.036l1.883-.272a4.6 4.6 0 0 0-.577-1.706a4.6 4.6 0 0 0-1.189-1.353m-3.832 3.708a.942.942 0 1 0 1.884 0a.942.942 0 1 0-1.884 0"/></svg>

After

Width:  |  Height:  |  Size: 707 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

18
svelte.config.js Normal file
View File

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

6
vite.config.ts Normal file
View File

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});