forked from ProjectSegfault/website
add segfaultapi functionality and use postgres
This commit is contained in:
parent
daaca19af8
commit
a846dd1e2d
@ -1 +1 @@
|
|||||||
VITE_API_URL=https://api.projectsegfau.lt
|
AUTH_SECRET=myauthsecret # generate with https://generate-secret.vercel.app/32 or openssl rand -hex 32 on unix-like
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,4 +6,5 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
config/config.yml
|
||||||
package-lock.json
|
package-lock.json
|
30
compose.yml
30
compose.yml
@ -1,8 +1,28 @@
|
|||||||
services:
|
services:
|
||||||
website:
|
segfaultapi:
|
||||||
container_name: website
|
container_name: segfaultapi
|
||||||
image: realprojectsegfault/website
|
#image: realprojectsegfault/segfaultapi
|
||||||
restart: always
|
restart: always
|
||||||
#build: .
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- 6893:6893
|
||||||
|
volumes:
|
||||||
|
- ./config:/app/config
|
||||||
|
segfaultapi-db:
|
||||||
|
image: mongo
|
||||||
|
container_name: segfaultapi-db
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 27017:27017
|
||||||
|
volumes:
|
||||||
|
- segfaultapi-db-data:/data/db
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: $MONGO_USER
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: $MONGO_PASSWORD
|
||||||
|
MONGO_INITDB_DATABASE: segfaultapi
|
||||||
|
command: [--auth]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
segfaultapi-db-data:
|
12
config/config.example.yml
Normal file
12
config/config.example.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
db:
|
||||||
|
url: "postgres://user:password@host:5432/database"
|
||||||
|
|
||||||
|
app:
|
||||||
|
auth:
|
||||||
|
clientId: "authentik-client-id"
|
||||||
|
clientSecret: "authentik-client-secret"
|
||||||
|
issuer: "https://yourdomain.com/application/o/app-name/"
|
||||||
|
hcaptcha:
|
||||||
|
secret: "your-hcaptcha-secret"
|
||||||
|
sitekey: "your-hcaptcha-sitekey"
|
||||||
|
webhook: "your-discord-webhook-url"
|
16
package.json
16
package.json
@ -14,7 +14,10 @@
|
|||||||
"@iconify-json/simple-icons": "^1.1.39",
|
"@iconify-json/simple-icons": "^1.1.39",
|
||||||
"@sveltejs/adapter-node": "1.0.0",
|
"@sveltejs/adapter-node": "1.0.0",
|
||||||
"@sveltejs/kit": "1.0.0",
|
"@sveltejs/kit": "1.0.0",
|
||||||
|
"axios": "^1.2.2",
|
||||||
|
"consola": "^2.15.3",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
|
"discord-webhook-node": "^1.1.8",
|
||||||
"mdsvex": "^0.10.6",
|
"mdsvex": "^0.10.6",
|
||||||
"prettier": "^2.8.1",
|
"prettier": "^2.8.1",
|
||||||
"prettier-plugin-svelte": "^2.9.0",
|
"prettier-plugin-svelte": "^2.9.0",
|
||||||
@ -28,7 +31,16 @@
|
|||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"unocss": "^0.47.6",
|
"unocss": "^0.47.6",
|
||||||
"vite": "4.0.1"
|
"vite": "4.0.1",
|
||||||
|
"yaml": "^2.2.0"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/core": "^0.2.3",
|
||||||
|
"@auth/sveltekit": "^0.1.10",
|
||||||
|
"joi": "^17.7.0",
|
||||||
|
"pg": "^8.8.0",
|
||||||
|
"pg-hstore": "^2.3.4",
|
||||||
|
"sequelize": "^6.28.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
549
pnpm-lock.yaml
generated
549
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,10 +0,0 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
import { env } from "$env/dynamic/private";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
|
||||||
return {
|
|
||||||
state: await fetch(env.VITE_API_URL + "/api/v1/state/blog").then(
|
|
||||||
(res) => res.json()
|
|
||||||
).catch(() => ({}))
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,14 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { PageData } from "./$types";
|
|
||||||
|
|
||||||
export let data: PageData
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if data.state.enabled}
|
|
||||||
<slot />
|
|
||||||
{:else}
|
|
||||||
<div class="flex items-center gap-2 text-center justify-center mt-16">
|
|
||||||
<div class="i-fa6-solid:circle-info" />
|
|
||||||
<span>The blog is currently disabled.</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
@ -1,10 +0,0 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
import { env } from "$env/dynamic/private";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
|
||||||
return {
|
|
||||||
posts: await fetch(env.VITE_API_URL + "/api/v1/blog").then(
|
|
||||||
(res) => res.json()
|
|
||||||
).catch(() => ({}))
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,16 +0,0 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
import { env } from "$env/dynamic/private";
|
|
||||||
import { compile } from "mdsvex";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
|
||||||
return {
|
|
||||||
post: await fetch(env.VITE_API_URL + "/api/v1/blog/" + params.title).then(
|
|
||||||
(res) => res.json()
|
|
||||||
).catch(() => ({})),
|
|
||||||
content: await fetch(env.VITE_API_URL + "/api/v1/blog/" + params.title)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((res) => compile(res.content))
|
|
||||||
.then((res) => res?.code)
|
|
||||||
.catch(() => ({})),
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
import { env } from "$env/dynamic/private";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
|
||||||
return {
|
|
||||||
authors: await fetch(env.VITE_API_URL + "/api/v1/blog/authors").then(
|
|
||||||
(res) => res.json()
|
|
||||||
).catch(() => ({})),
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,11 +0,0 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
import { env } from "$env/dynamic/private";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
|
||||||
return {
|
|
||||||
posts: await fetch(env.VITE_API_URL + "/api/v1/blog/authors/" + params.author).then(
|
|
||||||
(res) => res.json()
|
|
||||||
).catch(() => ({})),
|
|
||||||
authorName: params.author
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
import { env } from "$env/dynamic/private";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
|
||||||
return {
|
|
||||||
tags: await fetch(env.VITE_API_URL + "/api/v1/blog/tags").then(
|
|
||||||
(res) => res.json()
|
|
||||||
).catch(() => ({})),
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,11 +0,0 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
|
||||||
import { env } from "$env/dynamic/private";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
|
||||||
return {
|
|
||||||
posts: await fetch(env.VITE_API_URL + "/api/v1/blog/tags/" + params.tag).then(
|
|
||||||
(res) => res.json()
|
|
||||||
).catch(() => ({})),
|
|
||||||
tagName: params.tag
|
|
||||||
};
|
|
||||||
};
|
|
14
src/hooks.server.ts
Normal file
14
src/hooks.server.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { SvelteKitAuth } from "@auth/sveltekit"
|
||||||
|
import Authentik from '@auth/core/providers/authentik';
|
||||||
|
import config from "$lib/config";
|
||||||
|
|
||||||
|
export const handle = SvelteKitAuth({
|
||||||
|
providers: [
|
||||||
|
//@ts-ignore
|
||||||
|
Authentik({
|
||||||
|
clientId: config.app.auth.clientId,
|
||||||
|
clientSecret: config.app.auth.clientSecret,
|
||||||
|
issuer: config.app.auth.issuer
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import HCaptcha from "svelte-hcaptcha";
|
import HCaptcha from "svelte-hcaptcha";
|
||||||
import { Note } from "$lib/Form";
|
import { Note } from "$lib/Form";
|
||||||
|
import config from "$lib/config";
|
||||||
|
|
||||||
let submit = false;
|
let submit = false;
|
||||||
|
|
||||||
@ -14,13 +15,18 @@
|
|||||||
icon="i-fa6-solid:circle-info"
|
icon="i-fa6-solid:circle-info"
|
||||||
/>
|
/>
|
||||||
<HCaptcha
|
<HCaptcha
|
||||||
sitekey="41a7e3f9-595b-494e-ad73-150c410d4a51"
|
sitekey={config.app.hcaptcha.sitekey}
|
||||||
on:success={showSubmitButton}
|
on:success={showSubmitButton}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
{#if submit}
|
{#if submit}
|
||||||
<input
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Submit"
|
value="Submit"
|
||||||
class="form-button"
|
class="form-button"
|
||||||
/>
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -36,8 +36,7 @@
|
|||||||
{ name: "Contact us", url: "/contact" },
|
{ name: "Contact us", url: "/contact" },
|
||||||
{ name: "Our team", url: "/team" },
|
{ name: "Our team", url: "/team" },
|
||||||
{ name: "Timeline", url: "/timeline" },
|
{ name: "Timeline", url: "/timeline" },
|
||||||
//{ name: "Blog", url: "/blog" },
|
{ name: "Blog", url: "/blog" },
|
||||||
{ name: "Blog", url: "https://blog.projectsegfau.lt/", external: true },
|
|
||||||
{ name: "Legal", url: "/legal" },
|
{ name: "Legal", url: "/legal" },
|
||||||
{
|
{
|
||||||
name: "Status",
|
name: "Status",
|
||||||
|
24
src/lib/config.ts
Normal file
24
src/lib/config.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { parse } from "yaml";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
db: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
app: {
|
||||||
|
auth: {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
issuer: string;
|
||||||
|
}
|
||||||
|
hcaptcha: {
|
||||||
|
secret: string;
|
||||||
|
sitekey: string;
|
||||||
|
};
|
||||||
|
webhook: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: Config = parse(fs.readFileSync("./config/config.yml", "utf8"));
|
||||||
|
|
||||||
|
export default config;
|
74
src/lib/db.ts
Normal file
74
src/lib/db.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { Sequelize, DataTypes } from "sequelize";
|
||||||
|
import config from "$lib/config";
|
||||||
|
import consola from "consola";
|
||||||
|
|
||||||
|
const sequelize = new Sequelize(config.db.url);
|
||||||
|
|
||||||
|
sequelize.define("Announcements", {
|
||||||
|
title: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
severity: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
created: {
|
||||||
|
type: DataTypes.BIGINT,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sequelize.define("Posts", {
|
||||||
|
title: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
created: {
|
||||||
|
type: DataTypes.BIGINT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updated: {
|
||||||
|
type: DataTypes.BIGINT,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
|
},
|
||||||
|
words: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
readingTime: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
await sequelize.sync();
|
||||||
|
consola.success("Connected to Postgres");
|
||||||
|
} catch (error) {
|
||||||
|
consola.error("Failed to connect to Postgres:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default sequelize;
|
@ -1,18 +1,18 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
import type { PageServerLoad } from "./$types";
|
||||||
import { compile } from "mdsvex";
|
import { compile } from "mdsvex";
|
||||||
import { env } from "$env/dynamic/private";
|
import db from "$lib/db";
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
return {
|
const Announcements = db.model("Announcements");
|
||||||
state: await fetch(
|
|
||||||
env.VITE_API_URL + "/api/v1/state/announcements"
|
const data = await Announcements.findAll().then((docs) => {
|
||||||
).then((res) => res.json()).catch(() => ({})),
|
return docs.map((doc) => doc.get());
|
||||||
announcements: await fetch(
|
});
|
||||||
env.VITE_API_URL + "/api/v1/announcements"
|
|
||||||
).then((res) => res.json()).catch(() => ({})),
|
if (data.length !== 0 || data[0] !== undefined) {
|
||||||
content: await fetch(env.VITE_API_URL + "/api/v1/announcements")
|
return {
|
||||||
.then((res) => res.json())
|
announcements: data[0],
|
||||||
.then((res) => compile(res.title))
|
content: compile(data[0]["title"]).then((compiled) => compiled?.code)
|
||||||
.then((res) => res?.code).catch(() => ({})),
|
}
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
|
@ -4,99 +4,95 @@
|
|||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
let announcements = data.announcements;
|
let announcements = data.announcements;
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data.state.enabled}
|
{#if announcements}
|
||||||
{#if !announcements.error}
|
<div class="announcements">
|
||||||
<div class="announcements">
|
<div class="flex justify-center mt-16">
|
||||||
<div class="flex justify-center mt-16">
|
<div
|
||||||
|
class="announcement !text-[#252525] p-6 rounded-2 w-fit flex flex-col gap-4"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="announcement !text-[#252525] p-6 rounded-2 w-fit flex flex-col gap-4"
|
class="flex gap-4 flex-col sm:flex-row border-b-2 p-2 pt-0"
|
||||||
>
|
>
|
||||||
<div
|
{#if announcements.severity === "info"}
|
||||||
class="flex gap-4 flex-col sm:flex-row border-b-2 p-2 pt-0"
|
<div class="flex items-center gap-2">
|
||||||
>
|
<div class="i-fa6-solid:circle-info" />
|
||||||
{#if announcements.severity === "info"}
|
<span>Info</span>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="i-fa6-solid:circle-info" />
|
|
||||||
<span>Info</span>
|
|
||||||
</div>
|
|
||||||
{:else if announcements.severity === "low"}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="i-fa6-solid:check" />
|
|
||||||
<span>Resolved</span>
|
|
||||||
</div>
|
|
||||||
{:else if announcements.severity === "medium"}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="i-fa6-solid:triangle-exclamation" />
|
|
||||||
<span>Attention</span>
|
|
||||||
</div>
|
|
||||||
{:else if announcements.severity === "high"}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="i-fa6-solid:ban" />
|
|
||||||
<span>Attention</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<div class="i-fa6-solid:user" />
|
|
||||||
{announcements.author}
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<div class="i-fa6-solid:calendar" />
|
|
||||||
{dayjs
|
|
||||||
.unix(announcements.created)
|
|
||||||
.format("DD/MM/YYYY HH:mm")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="title">
|
|
||||||
<div class="text-xl font-semibold font-primary">
|
|
||||||
{@html data.content}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else if announcements.severity === "low"}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
{#if announcements.link}
|
<div class="i-fa6-solid:check" />
|
||||||
<div class="read-more">
|
<span>Resolved</span>
|
||||||
<a
|
</div>
|
||||||
href={announcements.link}
|
{:else if announcements.severity === "medium"}
|
||||||
class="!text-[#252525]">Read more...</a
|
<div class="flex items-center gap-2">
|
||||||
>
|
<div class="i-fa6-solid:triangle-exclamation" />
|
||||||
|
<span>Attention</span>
|
||||||
|
</div>
|
||||||
|
{:else if announcements.severity === "high"}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="i-fa6-solid:ban" />
|
||||||
|
<span>Attention</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<div class="i-fa6-solid:user" />
|
||||||
|
{announcements.author}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<div class="i-fa6-solid:calendar" />
|
||||||
|
{dayjs
|
||||||
|
.unix(announcements.created)
|
||||||
|
.format("DD/MM/YYYY HH:mm")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
<div class="text-xl font-semibold font-primary">
|
||||||
|
{@html data.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if announcements.link}
|
||||||
|
<div class="read-more">
|
||||||
|
<a
|
||||||
|
href={announcements.link}
|
||||||
|
class="!text-[#252525]">Read more...</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if announcements.severity === "info"}
|
|
||||||
<style>
|
|
||||||
.announcement {
|
|
||||||
background-color: #8caaee;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{:else if announcements.severity === "low"}
|
|
||||||
<style>
|
|
||||||
.announcement {
|
|
||||||
background-color: #a6d189;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{:else if announcements.severity === "medium"}
|
|
||||||
<style>
|
|
||||||
.announcement {
|
|
||||||
background-color: #e5c890;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{:else if announcements.severity === "high"}
|
|
||||||
<style>
|
|
||||||
.announcement {
|
|
||||||
background-color: #e78284;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<div class="flex items-center gap-2 text-center justify-center mt-16">
|
|
||||||
<div class="i-fa6-solid:circle-info" />
|
|
||||||
<span>Announcements are currently disabled.</span>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
{#if announcements.severity === "info"}
|
||||||
|
<style>
|
||||||
|
.announcement {
|
||||||
|
background-color: #8caaee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{:else if announcements.severity === "low"}
|
||||||
|
<style>
|
||||||
|
.announcement {
|
||||||
|
background-color: #a6d189;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{:else if announcements.severity === "medium"}
|
||||||
|
<style>
|
||||||
|
.announcement {
|
||||||
|
background-color: #e5c890;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{:else if announcements.severity === "high"}
|
||||||
|
<style>
|
||||||
|
.announcement {
|
||||||
|
background-color: #e78284;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
8
src/routes/admin/+layout.server.ts
Normal file
8
src/routes/admin/+layout.server.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import type { LayoutServerLoad } from "./$types"
|
||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
|
if (!await locals.getSession()) {
|
||||||
|
throw redirect(302, "/login");
|
||||||
|
}
|
||||||
|
}
|
10
src/routes/admin/+layout.svelte
Normal file
10
src/routes/admin/+layout.svelte
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<slot />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.col) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: fit-content;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
6
src/routes/admin/+page.svelte
Normal file
6
src/routes/admin/+page.svelte
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<h1>Admin dashboard</h1>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<a href="/admin/announcements">Announcements</a>
|
||||||
|
<a href="/admin/blog">Blog</a>
|
||||||
|
</div>
|
46
src/routes/admin/announcements/+page.server.ts
Normal file
46
src/routes/admin/announcements/+page.server.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import type { Actions } from "./$types";
|
||||||
|
import Joi from "joi";
|
||||||
|
import { fail } from "@sveltejs/kit";
|
||||||
|
import db from "$lib/db";
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
add: async ({ request }) => {
|
||||||
|
const Announcements = db.model("Announcements");
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
const BodyTypeSchema = Joi.object({
|
||||||
|
title: Joi.string().required(),
|
||||||
|
severity: Joi.string().required(),
|
||||||
|
author: Joi.string().required(),
|
||||||
|
link: Joi.string().optional().allow("")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) {
|
||||||
|
return fail(400, { addError: true, addMessage: String(BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) });
|
||||||
|
} else {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const data = {
|
||||||
|
...Object.fromEntries(formData.entries()),
|
||||||
|
created: now
|
||||||
|
};
|
||||||
|
|
||||||
|
await Announcements.sync();
|
||||||
|
|
||||||
|
await Announcements.destroy({ where: {} });
|
||||||
|
|
||||||
|
await Announcements.create(data);
|
||||||
|
|
||||||
|
return { addSuccess: true, addMessage: "Your announcement has been posted." };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete: async () => {
|
||||||
|
const Announcements = db.model("Announcements");
|
||||||
|
|
||||||
|
await Announcements.sync();
|
||||||
|
|
||||||
|
await Announcements.destroy({ where: {} });
|
||||||
|
|
||||||
|
return { deleteSuccess: true, deleteMessage: "Your announcement has been deleted." };
|
||||||
|
}
|
||||||
|
}
|
55
src/routes/admin/announcements/+page.svelte
Normal file
55
src/routes/admin/announcements/+page.svelte
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ActionData } from '.$/types';
|
||||||
|
|
||||||
|
export let form: ActionData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<h1>Post Announcement</h1>
|
||||||
|
<form
|
||||||
|
action="?/add"
|
||||||
|
method="POST"
|
||||||
|
class="col"
|
||||||
|
>
|
||||||
|
<select id="severity" name="severity" required>
|
||||||
|
<option value="" selected disabled>
|
||||||
|
Select severity of announcement
|
||||||
|
</option>
|
||||||
|
<option value="info">Information announcement</option>
|
||||||
|
<option value="low">Low severity</option>
|
||||||
|
<option value="medium">Medium severity</option>
|
||||||
|
<option value="high">High severity</option>
|
||||||
|
</select>
|
||||||
|
<textarea
|
||||||
|
name="title"
|
||||||
|
rows="4"
|
||||||
|
cols="25"
|
||||||
|
placeholder="The announcement text"
|
||||||
|
></textarea>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="link"
|
||||||
|
placeholder="Your link for more details"
|
||||||
|
/>
|
||||||
|
<input type="text" name="author" placeholder="Your name" />
|
||||||
|
{#if form?.addSuccess}
|
||||||
|
{form.addMessage}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if form?.addError}
|
||||||
|
{form.addMessage}
|
||||||
|
{/if}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
<h1 style="margin-top: 20px">Delete Announcement</h1>
|
||||||
|
<form
|
||||||
|
action="?/delete"
|
||||||
|
method="POST"
|
||||||
|
class="col"
|
||||||
|
>
|
||||||
|
{#if form?.deleteSuccess}
|
||||||
|
{form.deleteMessage}
|
||||||
|
{/if}
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
143
src/routes/admin/blog/+page.server.ts
Normal file
143
src/routes/admin/blog/+page.server.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import type { Actions, PageServerLoad } from "./$types";
|
||||||
|
import db from "$lib/db";
|
||||||
|
import Joi from "joi";
|
||||||
|
import { fail } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load = ( async () => {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
return {
|
||||||
|
postTitles: await Posts.findAll({ attributes: ["title"] }).then((docs) => {
|
||||||
|
const titles = docs.map((doc) => doc.get("title"));
|
||||||
|
return titles;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
add: async ({ request }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
const AddPostTypeSchema = Joi.object({
|
||||||
|
title: Joi.string().required(),
|
||||||
|
content: Joi.string().required(),
|
||||||
|
tags: Joi.string().optional().allow(""),
|
||||||
|
author: Joi.string().required()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (AddPostTypeSchema.validate(Object.fromEntries(formData.entries())).error) {
|
||||||
|
return fail(400, { addError: true, addMessage: String(AddPostTypeSchema.validate(Object.fromEntries(formData.entries())).error) });
|
||||||
|
} else {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const words = formData.get("content")!.trim().split(/\s+/).length;
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const tags = formData.get("tags") ? formData.get("tags").split(" ") : [];
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
title: formData.get("title"),
|
||||||
|
content: formData.get("content"),
|
||||||
|
tags: tags,
|
||||||
|
author: formData.get("author"),
|
||||||
|
created: now,
|
||||||
|
words: words,
|
||||||
|
readingTime: Math.ceil(words / 225)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (await Posts.findOne({ where: { title: data.title } })) {
|
||||||
|
return fail(409, { addError: true, addMessage: "A post with that title already exists." });
|
||||||
|
} else {
|
||||||
|
await Posts.create(data);
|
||||||
|
|
||||||
|
return { addSuccess: true, addMessage: "Your post has been posted." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete: async ({ request }) => {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
const deleteFromDb = await Posts.destroy({ where: { title: formData.get("title") } });
|
||||||
|
|
||||||
|
if (!deleteFromDb) {
|
||||||
|
return fail(404, { deleteError: true, deleteMessage: "A post with that title does not exist." });
|
||||||
|
} else {
|
||||||
|
return { deleteSuccess: true, deleteMessage: "Your post has been deleted." };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edit: async ({ request }) => {
|
||||||
|
const EditPostTypeSchema = Joi.object({
|
||||||
|
title: Joi.string().required(),
|
||||||
|
newTitle: Joi.string().optional().allow(""),
|
||||||
|
content: Joi.string().optional().allow(""),
|
||||||
|
tags: Joi.string().optional().allow(""),
|
||||||
|
area: Joi.string().required().allow("title", "content", "tags")
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
if (EditPostTypeSchema.validate(Object.fromEntries(formData.entries())).error) {
|
||||||
|
return fail(400, { editError: true, editMessage: String(EditPostTypeSchema.validate(Object.fromEntries(formData.entries())).error) });
|
||||||
|
} else {
|
||||||
|
if (formData.get("area") === "title") {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
const updateOnDb = await Posts.update(
|
||||||
|
{ title: formData.get("newTitle") },
|
||||||
|
{ where: { title: formData.get("title") } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updateOnDb[0] === 0) {
|
||||||
|
return fail(404, { editError: true, editMessage: "A post with that title does not exist." });
|
||||||
|
} else {
|
||||||
|
return { editSuccess: true, editMessage: "Your post has been edited." };
|
||||||
|
}
|
||||||
|
} else if (formData.get("area") === "content") {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const words = formData.get("content")!.trim().split(/\s+/).length;
|
||||||
|
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const updateonDb = await Posts.update(
|
||||||
|
{
|
||||||
|
content: formData.get("content"),
|
||||||
|
words: words,
|
||||||
|
readingTime: Math.ceil(words / 225),
|
||||||
|
updated: now
|
||||||
|
},
|
||||||
|
{ where: { title: formData.get("title") } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updateonDb[0] === 0) {
|
||||||
|
return fail(404, { editError: true, editMessage: "A post with that title does not exist." });
|
||||||
|
} else {
|
||||||
|
return { editSuccess: true, editMessage: "Your post has been edited." };
|
||||||
|
}
|
||||||
|
} else if (formData.get("area") === "tags") {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const tags = formData.get("tags") ? formData.get("tags").split(" ") : [];
|
||||||
|
|
||||||
|
const updateOnDb = await Posts.update(
|
||||||
|
{ tags: tags
|
||||||
|
},
|
||||||
|
{ where: { title: formData.get("title") } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updateOnDb[0] === 0) {
|
||||||
|
return fail(404, { editError: true, editMessage: "A post with that title does not exist." });
|
||||||
|
} else {
|
||||||
|
return { editSuccess: true, editMessage: "Your post has been edited." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
81
src/routes/admin/blog/+page.svelte
Normal file
81
src/routes/admin/blog/+page.svelte
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ActionData, PageData } from '.$/types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
export let form: ActionData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>Add post</h1>
|
||||||
|
|
||||||
|
<form action="?/add" method="POST" class="col">
|
||||||
|
<input type="text" name="title" placeholder="Title" required />
|
||||||
|
<textarea
|
||||||
|
name="content"
|
||||||
|
placeholder="Content"
|
||||||
|
rows="4"
|
||||||
|
cols="25"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
<input type="text" name="tags" placeholder="Tags" />
|
||||||
|
<input type="text" name="author" placeholder="Author" required />
|
||||||
|
{#if form?.addSuccess}
|
||||||
|
{form.addMessage}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if form?.addError}
|
||||||
|
{form.addMessage}
|
||||||
|
{/if}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h1>Delete post</h1>
|
||||||
|
|
||||||
|
<form action="?/delete" method="POST" class="col">
|
||||||
|
<select name="title" required>
|
||||||
|
{#each data.postTitles as title}
|
||||||
|
<option value="{title}">{title}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if form?.deleteSuccess}
|
||||||
|
{form.deleteMessage}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if form?.deleteError}
|
||||||
|
{form.deleteMessage}
|
||||||
|
{/if}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h1>Edit post</h1>
|
||||||
|
|
||||||
|
<form action="?/edit" method="POST" class="col">
|
||||||
|
<select name="title" required>
|
||||||
|
<option disabled>Post title</option>
|
||||||
|
{#each data.postTitles as title}
|
||||||
|
<option value="{title}">{title}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<select name="area">
|
||||||
|
<option disabled>Area to change</option>
|
||||||
|
<option value="title">Title</option>
|
||||||
|
<option value="content">Content</option>
|
||||||
|
<option value="tags">Tags</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="newTitle" placeholder="New title" />
|
||||||
|
<textarea
|
||||||
|
name="content"
|
||||||
|
placeholder="New content"
|
||||||
|
rows="4"
|
||||||
|
cols="25"
|
||||||
|
></textarea>
|
||||||
|
<input type="text" name="tags" placeholder="New tags" />
|
||||||
|
{#if form?.editSuccess}
|
||||||
|
{form.editMessage}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if form?.editError}
|
||||||
|
{form.editMessage}
|
||||||
|
{/if}
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
25
src/routes/api/status/+server.ts
Normal file
25
src/routes/api/status/+server.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import statusData from "./statusData";
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
|
||||||
|
const updateMap = () => {
|
||||||
|
map.set("data", {
|
||||||
|
status: statusData,
|
||||||
|
updated: Math.floor(Date.now() / 1000)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMap();
|
||||||
|
|
||||||
|
setInterval(updateMap, 30000);
|
||||||
|
|
||||||
|
export const GET = (() => {
|
||||||
|
const data = map.get("data");
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json; charset=utf-8"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}) satisfies RequestHandler;
|
119
src/routes/api/status/statusData.ts
Normal file
119
src/routes/api/status/statusData.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const fetchStatus = (domain: string) => {
|
||||||
|
const req = axios("https://" + domain, { timeout: 10000 })
|
||||||
|
.then((res) => res.status)
|
||||||
|
.catch((err) => err.response.status);
|
||||||
|
|
||||||
|
return req;
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusData = [
|
||||||
|
{
|
||||||
|
name: "Privacy front-ends",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: "Invidious",
|
||||||
|
description: "A frontend for YouTube.",
|
||||||
|
link: "https://invidious.projectsegfau.lt/",
|
||||||
|
us: "https://inv.us.projectsegfau.lt",
|
||||||
|
bp: "https://inv.bp.projectsegfau.lt",
|
||||||
|
icon: "https://github.com/iv-org/invidious/raw/master/assets/invidious-colored-vector.svg",
|
||||||
|
status: await fetchStatus("invidious.projectsegfau.lt"),
|
||||||
|
statusUs: await fetchStatus("inv.us.projectsegfau.lt"),
|
||||||
|
statusBp: await fetchStatus("inv.bp.projectsegfau.lt")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Librarian",
|
||||||
|
description: "A frontend for Odysee.",
|
||||||
|
link: "https://lbry.projectsegfau.lt/",
|
||||||
|
icon: "https://codeberg.org/avatars/dd785d92b4d4df06d448db075cd29274",
|
||||||
|
status: await fetchStatus("lbry.projectsegfau.lt")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Libreddit",
|
||||||
|
description: "A frontend for Reddit.",
|
||||||
|
link: "https://libreddit.projectsegfau.lt/",
|
||||||
|
us: "https://libreddit.us.projectsegfau.lt",
|
||||||
|
icon: "https://github.com/spikecodes/libreddit/raw/master/static/logo.png",
|
||||||
|
status: await fetchStatus("libreddit.projectsegfau.lt"),
|
||||||
|
statusUs: await fetchStatus("libreddit.us.projectsegfau.lt")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nitter",
|
||||||
|
description: "A frontend for Twitter.",
|
||||||
|
link: "https://nitter.projectsegfau.lt/",
|
||||||
|
us: "https://nitter.us.projectsegfau.lt",
|
||||||
|
icon: "https://github.com/zedeus/nitter/raw/master/public/logo.png",
|
||||||
|
status: await fetchStatus("nitter.projectsegfau.lt"),
|
||||||
|
statusUs: await fetchStatus("nitter.us.projectsegfau.lt")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Piped",
|
||||||
|
description: "Another frontend for YouTube.",
|
||||||
|
link: "https://piped.projectsegfau.lt/",
|
||||||
|
us: "https://piped.us.projectsegfau.lt",
|
||||||
|
icon: "https://github.com/TeamPiped/Piped/raw/master/public/img/icons/logo.svg",
|
||||||
|
status: await fetchStatus("piped.projectsegfau.lt"),
|
||||||
|
statusUs: await fetchStatus("piped.us.projectsegfau.lt")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Useful tools and services",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: "Element",
|
||||||
|
description:
|
||||||
|
"An open source and decentralized chat application.",
|
||||||
|
link: "https://chat.projectsegfau.lt/",
|
||||||
|
icon: "https://element.io/images/logo-mark-primary.svg",
|
||||||
|
status: await fetchStatus("chat.projectsegfau.lt")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SearXNG",
|
||||||
|
description: "A private meta-search engine.",
|
||||||
|
link: "https://search.projectsegfau.lt/search",
|
||||||
|
us: "https://search.us.projectsegfau.lt",
|
||||||
|
icon: "https://docs.searxng.org/_static/searxng-wordmark.svg",
|
||||||
|
status: await fetchStatus("search.projectsegfau.lt"),
|
||||||
|
statusUs: await fetchStatus("search.us.projectsegfau.lt")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Gitea",
|
||||||
|
description: "A web interface for Git, alternative to GitHub.",
|
||||||
|
link: "https://git.projectsegfau.lt/",
|
||||||
|
icon: "https://gitea.io/images/gitea.png",
|
||||||
|
status: await fetchStatus("git.projectsegfau.lt")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Internal services",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
name: "Portainer",
|
||||||
|
description: "Portainer instance for our servers.",
|
||||||
|
link: "https://portainer.projectsegfau.lt/",
|
||||||
|
icon: "https://avatars.githubusercontent.com/u/22225832",
|
||||||
|
status: await fetchStatus("portainer.projectsegfau.lt")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mailcow",
|
||||||
|
description: "Our mail server and webmail.",
|
||||||
|
link: "https://mail.projectsegfau.lt/",
|
||||||
|
icon: "https://mailcow.email/images/cow_mailcow.svg",
|
||||||
|
status: await fetchStatus("mail.projectsegfau.lt")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Plausible analytics",
|
||||||
|
description: "Analytics for our website.",
|
||||||
|
link: "https://analytics.projectsegfau.lt/projectsegfau.lt",
|
||||||
|
icon: "https://avatars.githubusercontent.com/u/54802774",
|
||||||
|
status: await fetchStatus("analytics.projectsegfau.lt")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default statusData;
|
20
src/routes/blog/+page.server.ts
Normal file
20
src/routes/blog/+page.server.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import db from "$lib/db";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async () => {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
const posts = await Posts.findAll().then((docs) => {
|
||||||
|
return docs.map((doc) => doc.get());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (posts.length === 0 || posts[0] === undefined) {
|
||||||
|
return {
|
||||||
|
posts: []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
posts: posts.sort((a, b) => b["created"] - a["created"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -7,10 +7,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Timeline | Project Segfault</title>
|
<title>Blog | Project Segfault</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Timeline of Project Segfault's history."
|
content="Project Segfault's blog"
|
||||||
/>
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
27
src/routes/blog/[title]/+page.server.ts
Normal file
27
src/routes/blog/[title]/+page.server.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import db from "$lib/db";
|
||||||
|
import { compile } from "mdsvex";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
const data = await Posts.findAll({
|
||||||
|
where: {
|
||||||
|
title: params.title
|
||||||
|
}
|
||||||
|
}).then((docs) => {
|
||||||
|
return docs.map((doc) => doc.get());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.length === 0 || data[0] === undefined) {
|
||||||
|
return {
|
||||||
|
post: {},
|
||||||
|
content: {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
post: data[0],
|
||||||
|
content: compile(data[0].content).then((res) => res?.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
23
src/routes/blog/authors/+page.server.ts
Normal file
23
src/routes/blog/authors/+page.server.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import db from "$lib/db";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
const data = await Posts.findAll({
|
||||||
|
attributes: ["author"]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.length === 0 || data[0] === undefined) {
|
||||||
|
return {
|
||||||
|
authors: []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const authors = data.map((post) => post["author"]);
|
||||||
|
const uniqueAuthors = [...new Set(authors)];
|
||||||
|
|
||||||
|
return {
|
||||||
|
authors: uniqueAuthors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
26
src/routes/blog/authors/[author]/+page.server.ts
Normal file
26
src/routes/blog/authors/[author]/+page.server.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import db from "$lib/db";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
const data = await Posts.findAll({
|
||||||
|
where: {
|
||||||
|
author: params.author
|
||||||
|
}
|
||||||
|
}).then((docs) => {
|
||||||
|
return docs.map((doc) => doc.get());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.length === 0 || data[0] === undefined) {
|
||||||
|
return {
|
||||||
|
posts: [],
|
||||||
|
authorName: params.author
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
posts: data,
|
||||||
|
authorName: params.author
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
20
src/routes/blog/tags/+page.server.ts
Normal file
20
src/routes/blog/tags/+page.server.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import db from "$lib/db";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async () => {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
const data = await Posts.findAll({ attributes: ["tags"] })
|
||||||
|
|
||||||
|
if (data.length === 0 || data[0] === undefined) {
|
||||||
|
return {
|
||||||
|
tags: []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const tags = data.map((post) => post["tags"]).flat();
|
||||||
|
const uniqueTags = [...new Set(tags)];
|
||||||
|
return {
|
||||||
|
tags: uniqueTags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
29
src/routes/blog/tags/[tag]/+page.server.ts
Normal file
29
src/routes/blog/tags/[tag]/+page.server.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import db from "$lib/db";
|
||||||
|
import { Op } from "sequelize";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const Posts = db.model("Posts");
|
||||||
|
|
||||||
|
const data = await Posts.findAll({
|
||||||
|
where: {
|
||||||
|
tags: {
|
||||||
|
[Op.contains]: [params.tag]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then((docs) => {
|
||||||
|
return docs.map((doc) => doc.get());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.length === 0 || data[0] === undefined) {
|
||||||
|
return {
|
||||||
|
posts: [],
|
||||||
|
tagName: params.tag
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
posts: data,
|
||||||
|
tagName: params.tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -1,11 +1,64 @@
|
|||||||
import type { PageServerLoad } from "../$types";
|
import type { Actions } from "./$types";
|
||||||
import { env } from "$env/dynamic/private";
|
import { Webhook, MessageBuilder } from "discord-webhook-node";
|
||||||
|
import Joi from "joi";
|
||||||
|
import { fail } from "@sveltejs/kit";
|
||||||
|
import config from "$lib/config";
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const actions: Actions = {
|
||||||
return {
|
form: async ({ request, getClientAddress, fetch }) => {
|
||||||
state: await fetch(env.VITE_API_URL + "/api/v1/state/form").then(
|
const formData = await request.formData();
|
||||||
(res) => res.json()
|
|
||||||
).catch(() => ({})),
|
const BodyTypeSchema = Joi.object({
|
||||||
apiUrl: env.VITE_API_URL
|
email: Joi.string().email().required(),
|
||||||
};
|
commentType: Joi.string().required(),
|
||||||
};
|
message: Joi.string().required(),
|
||||||
|
"h-captcha-response": Joi.string().required(),
|
||||||
|
"g-recaptcha-response": Joi.string().optional().allow("")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) {
|
||||||
|
return fail(400, { error: true, message: String(BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) });
|
||||||
|
} else {
|
||||||
|
const ip = getClientAddress();
|
||||||
|
|
||||||
|
const verify = await fetch("https://hcaptcha.com/siteverify", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
secret: config.app.hcaptcha.secret,
|
||||||
|
response: String(formData.get("h-captcha-response")),
|
||||||
|
remoteip: ip
|
||||||
|
})
|
||||||
|
}).then((res) => res.json())
|
||||||
|
|
||||||
|
|
||||||
|
const hook = new Webhook(config.app.webhook);
|
||||||
|
|
||||||
|
const data = await verify;
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const embed = new MessageBuilder()
|
||||||
|
.setAuthor(
|
||||||
|
`${ip}, ${formData.get("email")}, https://abuseipdb.com/check/${ip}`
|
||||||
|
)
|
||||||
|
// @ts-ignore
|
||||||
|
.addField("Comment type", formData.get("commentType"), true)
|
||||||
|
// @ts-ignore
|
||||||
|
.addField("Message", formData.get("message"))
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
hook.send(embed);
|
||||||
|
|
||||||
|
return { success: true, message: "Thanks for your message, we will get back to you as soon as possible." };
|
||||||
|
} else {
|
||||||
|
hook.send(
|
||||||
|
`IP: ${ip}, https://abuseipdb.com/check/${ip}\nfailed to complete the captcha with error: ${data["error-codes"]}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
return fail(400, { error: true, message: "Captcha failed or expired, please try again. If this keeps happening, assume the captcha is broken and contact us on Matrix." + " Error: " + data["error-codes"] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,58 +1,58 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Note, Captcha, Form, Meta, TextArea } from "$lib/Form";
|
import { Note, Captcha, Meta, TextArea } from "$lib/Form";
|
||||||
import type { PageData } from "./$types";
|
import type { ActionData } from "./$types";
|
||||||
|
|
||||||
export let data: PageData;
|
export let form: ActionData;
|
||||||
|
|
||||||
let title = "Contact us | Project Segfault";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{title}</title>
|
<title>Contact us | Project Segfault</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1>Contact us</h1>
|
<h1>Contact us</h1>
|
||||||
|
|
||||||
<div class="contact-form">
|
<div class="contact-form">
|
||||||
<h2>Contact form</h2>
|
<h2>Contact form</h2>
|
||||||
{#if data.state.enabled === true}
|
<form
|
||||||
<Form
|
method="POST"
|
||||||
action="{data.apiUrl}/api/v1/form"
|
action="?/form"
|
||||||
method="POST"
|
id="contact-form"
|
||||||
id="contact-form"
|
class="flex flex-col gap-4 w-fit"
|
||||||
|
>
|
||||||
|
<Note
|
||||||
|
content="Your IP will be logged for anti-abuse measures."
|
||||||
|
icon="i-fa6-solid:lock"
|
||||||
|
/>
|
||||||
|
<Meta
|
||||||
|
inputType="email"
|
||||||
|
inputPlaceholder="Your email"
|
||||||
|
selectType="commentType"
|
||||||
>
|
>
|
||||||
<Note
|
<option
|
||||||
content="Your IP will be logged for anti-abuse measures."
|
value=""
|
||||||
icon="i-fa6-solid:lock"
|
selected
|
||||||
/>
|
disabled>Select a type of comment</option
|
||||||
<Meta
|
|
||||||
inputType="email"
|
|
||||||
inputPlaceholder="Your email"
|
|
||||||
selectType="commentType"
|
|
||||||
>
|
>
|
||||||
<option
|
<option value="Feedback">Feedback</option>
|
||||||
value=""
|
<option value="Suggestion">Suggestion</option>
|
||||||
selected
|
<option value="Question">Question</option>
|
||||||
disabled>Select a type of comment</option
|
<option value="Bug">Bug</option>
|
||||||
>
|
</Meta>
|
||||||
<option value="Feedback">Feedback</option>
|
<TextArea
|
||||||
<option value="Suggestion">Suggestion</option>
|
id="comment"
|
||||||
<option value="Question">Question</option>
|
name="message"
|
||||||
<option value="Bug">Bug</option>
|
placeholder="Your message"
|
||||||
</Meta>
|
/>
|
||||||
<TextArea
|
<Captcha>
|
||||||
id="comment"
|
{#if form?.success}
|
||||||
name="message"
|
{form.message}
|
||||||
placeholder="Your message"
|
{/if}
|
||||||
/>
|
|
||||||
<Captcha />
|
{#if form?.error}
|
||||||
</Form>
|
{form.message}
|
||||||
{:else}
|
{/if}
|
||||||
<div class="flex items-center gap-2">
|
</Captcha>
|
||||||
<div class="i-fa6-solid:circle-info" />
|
</form>
|
||||||
<span>The contact form is currently disabled.</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<noscript>
|
<noscript>
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import type { PageServerLoad } from "./$types";
|
import type { PageServerLoad } from "./$types";
|
||||||
import { env } from "$env/dynamic/private";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async ({ fetch }) => {
|
||||||
return {
|
return {
|
||||||
state: await fetch(env.VITE_API_URL + "/api/v1/state/status").then(
|
instances: await fetch("/api/status").then(
|
||||||
(res) => res.json()
|
(res) => res.json()
|
||||||
).catch(() => ({})),
|
)
|
||||||
instances: await fetch(env.VITE_API_URL + "/api/v1/status").then(
|
|
||||||
(res) => res.json()
|
|
||||||
).catch(() => ({}))
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -17,58 +17,51 @@
|
|||||||
|
|
||||||
<h1>Our instances</h1>
|
<h1>Our instances</h1>
|
||||||
|
|
||||||
{#if data.state.enabled}
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex flex-col gap-4">
|
<CardOuter>
|
||||||
<CardOuter>
|
<div class="flex flex-col">
|
||||||
<div class="flex flex-col">
|
{#each data.instances.status as group}
|
||||||
{#each data.instances.status as group}
|
<h2>{group.name}</h2>
|
||||||
<h2>{group.name}</h2>
|
<div class="flex flex-row flex-wrap gap-8">
|
||||||
<div class="flex flex-row flex-wrap gap-8">
|
{#each group.data as item}
|
||||||
{#each group.data as item}
|
<CardInner
|
||||||
<CardInner
|
title={item.name}
|
||||||
title={item.name}
|
description={item.description}
|
||||||
description={item.description}
|
icon={item.icon}
|
||||||
icon={item.icon}
|
>
|
||||||
>
|
<LinksOuter>
|
||||||
<LinksOuter>
|
<InstanceLink
|
||||||
|
url={item.link}
|
||||||
|
item={item.status}
|
||||||
|
type="main"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if item.us}
|
||||||
<InstanceLink
|
<InstanceLink
|
||||||
url={item.link}
|
url={item.us}
|
||||||
item={item.status}
|
item={item.statusUs}
|
||||||
type="main"
|
type="us"
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if item.us}
|
{#if item.bp}
|
||||||
<InstanceLink
|
<InstanceLink
|
||||||
url={item.us}
|
url={item.bp}
|
||||||
item={item.statusUs}
|
item={item.statusBp}
|
||||||
type="us"
|
type="backup"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
</LinksOuter>
|
||||||
|
</CardInner>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</CardOuter>
|
||||||
|
|
||||||
{#if item.bp}
|
<span class="bg-accent w-fit p-2 rounded-2 text-primary"
|
||||||
<InstanceLink
|
>Instances status last updated: {dayjs
|
||||||
url={item.bp}
|
.unix(data.instances.updated)
|
||||||
item={item.statusBp}
|
.format("DD/MM/YYYY HH:mm:ss")}
|
||||||
type="backup"
|
</span>
|
||||||
/>
|
</div>
|
||||||
{/if}
|
|
||||||
</LinksOuter>
|
|
||||||
</CardInner>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</CardOuter>
|
|
||||||
|
|
||||||
<span class="bg-accent w-fit p-2 rounded-2 text-primary"
|
|
||||||
>Instances status last updated: {dayjs
|
|
||||||
.unix(data.instances.updated)
|
|
||||||
.format("DD/MM/YYYY HH:mm:ss")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex items-center gap-2 mt-16">
|
|
||||||
<div class="i-fa6-solid:circle-info" />
|
|
||||||
<span>Instances are currently disabled.</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
7
src/routes/login/+layout.server.ts
Normal file
7
src/routes/login/+layout.server.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { LayoutServerLoad } from "./$types"
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async (event) => {
|
||||||
|
return {
|
||||||
|
session: await event.locals.getSession(),
|
||||||
|
}
|
||||||
|
}
|
23
src/routes/login/+page.svelte
Normal file
23
src/routes/login/+page.svelte
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script>
|
||||||
|
import { signIn, signOut } from '@auth/sveltekit/client';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
const buttonStyles = "cursor-pointer border-none rounded-2 py-6 px-18 text-lg text-text bg-secondary hover:brightness-125 transition duration-250 font-primary"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{#if Object.keys($page.data.session || {}).length}
|
||||||
|
<div class="flex flex-col items-center justify-center gap-4 mt-28">
|
||||||
|
<div class="flex flex-row items-center gap-1">
|
||||||
|
<span>Signed in as</span><br />
|
||||||
|
<span class="font-extrabold">{$page?.data?.session?.user?.email}</span>
|
||||||
|
</div>
|
||||||
|
<a href="/admin">Go to admin dashboard</a>
|
||||||
|
<button on:click={() => signOut()} class={buttonStyles}>Sign out</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center justify-center gap-4 mt-28">
|
||||||
|
<span>You are not signed in</span>
|
||||||
|
<button on:click={() => signIn("authentik")} class={buttonStyles}>Sign in using Authentik</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
Loading…
Reference in New Issue
Block a user