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.example
|
||||
config/config.yml
|
||||
package-lock.json
|
30
compose.yml
30
compose.yml
@ -1,8 +1,28 @@
|
||||
services:
|
||||
website:
|
||||
container_name: website
|
||||
image: realprojectsegfault/website
|
||||
segfaultapi:
|
||||
container_name: segfaultapi
|
||||
#image: realprojectsegfault/segfaultapi
|
||||
restart: always
|
||||
#build: .
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
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",
|
||||
"@sveltejs/adapter-node": "1.0.0",
|
||||
"@sveltejs/kit": "1.0.0",
|
||||
"axios": "^1.2.2",
|
||||
"consola": "^2.15.3",
|
||||
"dayjs": "^1.11.7",
|
||||
"discord-webhook-node": "^1.1.8",
|
||||
"mdsvex": "^0.10.6",
|
||||
"prettier": "^2.8.1",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
@ -28,7 +31,16 @@
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.4",
|
||||
"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>
|
||||
import HCaptcha from "svelte-hcaptcha";
|
||||
import { Note } from "$lib/Form";
|
||||
import config from "$lib/config";
|
||||
|
||||
let submit = false;
|
||||
|
||||
@ -14,13 +15,18 @@
|
||||
icon="i-fa6-solid:circle-info"
|
||||
/>
|
||||
<HCaptcha
|
||||
sitekey="41a7e3f9-595b-494e-ad73-150c410d4a51"
|
||||
sitekey={config.app.hcaptcha.sitekey}
|
||||
on:success={showSubmitButton}
|
||||
/>
|
||||
|
||||
<slot />
|
||||
|
||||
{#if submit}
|
||||
<input
|
||||
<button
|
||||
type="submit"
|
||||
value="Submit"
|
||||
class="form-button"
|
||||
/>
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
{/if}
|
||||
|
@ -36,8 +36,7 @@
|
||||
{ name: "Contact us", url: "/contact" },
|
||||
{ name: "Our team", url: "/team" },
|
||||
{ name: "Timeline", url: "/timeline" },
|
||||
//{ name: "Blog", url: "/blog" },
|
||||
{ name: "Blog", url: "https://blog.projectsegfau.lt/", external: true },
|
||||
{ name: "Blog", url: "/blog" },
|
||||
{ name: "Legal", url: "/legal" },
|
||||
{
|
||||
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 { compile } from "mdsvex";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import db from "$lib/db";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
return {
|
||||
state: await fetch(
|
||||
env.VITE_API_URL + "/api/v1/state/announcements"
|
||||
).then((res) => res.json()).catch(() => ({})),
|
||||
announcements: await fetch(
|
||||
env.VITE_API_URL + "/api/v1/announcements"
|
||||
).then((res) => res.json()).catch(() => ({})),
|
||||
content: await fetch(env.VITE_API_URL + "/api/v1/announcements")
|
||||
.then((res) => res.json())
|
||||
.then((res) => compile(res.title))
|
||||
.then((res) => res?.code).catch(() => ({})),
|
||||
};
|
||||
const Announcements = db.model("Announcements");
|
||||
|
||||
const data = await Announcements.findAll().then((docs) => {
|
||||
return docs.map((doc) => doc.get());
|
||||
});
|
||||
|
||||
if (data.length !== 0 || data[0] !== undefined) {
|
||||
return {
|
||||
announcements: data[0],
|
||||
content: compile(data[0]["title"]).then((compiled) => compiled?.code)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -4,99 +4,95 @@
|
||||
export let data: PageData;
|
||||
|
||||
let announcements = data.announcements;
|
||||
|
||||
console.log(data);
|
||||
|
||||
import dayjs from "dayjs";
|
||||
</script>
|
||||
|
||||
{#if data.state.enabled}
|
||||
{#if !announcements.error}
|
||||
<div class="announcements">
|
||||
<div class="flex justify-center mt-16">
|
||||
{#if announcements}
|
||||
<div class="announcements">
|
||||
<div class="flex justify-center mt-16">
|
||||
<div
|
||||
class="announcement !text-[#252525] p-6 rounded-2 w-fit flex flex-col gap-4"
|
||||
>
|
||||
<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
|
||||
class="flex gap-4 flex-col sm:flex-row border-b-2 p-2 pt-0"
|
||||
>
|
||||
{#if announcements.severity === "info"}
|
||||
<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}
|
||||
{#if announcements.severity === "info"}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="i-fa6-solid:circle-info" />
|
||||
<span>Info</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if announcements.link}
|
||||
<div class="read-more">
|
||||
<a
|
||||
href={announcements.link}
|
||||
class="!text-[#252525]">Read more...</a
|
||||
>
|
||||
{: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>
|
||||
|
||||
{#if announcements.link}
|
||||
<div class="read-more">
|
||||
<a
|
||||
href={announcements.link}
|
||||
class="!text-[#252525]">Read more...</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</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>
|
||||
{/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>
|
||||
|
||||
<svelte:head>
|
||||
<title>Timeline | Project Segfault</title>
|
||||
<title>Blog | Project Segfault</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Timeline of Project Segfault's history."
|
||||
content="Project Segfault's blog"
|
||||
/>
|
||||
</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 { env } from "$env/dynamic/private";
|
||||
import type { Actions } from "./$types";
|
||||
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 () => {
|
||||
return {
|
||||
state: await fetch(env.VITE_API_URL + "/api/v1/state/form").then(
|
||||
(res) => res.json()
|
||||
).catch(() => ({})),
|
||||
apiUrl: env.VITE_API_URL
|
||||
};
|
||||
};
|
||||
export const actions: Actions = {
|
||||
form: async ({ request, getClientAddress, fetch }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const BodyTypeSchema = Joi.object({
|
||||
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">
|
||||
import { Note, Captcha, Form, Meta, TextArea } from "$lib/Form";
|
||||
import type { PageData } from "./$types";
|
||||
import { Note, Captcha, Meta, TextArea } from "$lib/Form";
|
||||
import type { ActionData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let title = "Contact us | Project Segfault";
|
||||
export let form: ActionData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
<title>Contact us | Project Segfault</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Contact us</h1>
|
||||
|
||||
<div class="contact-form">
|
||||
<h2>Contact form</h2>
|
||||
{#if data.state.enabled === true}
|
||||
<Form
|
||||
action="{data.apiUrl}/api/v1/form"
|
||||
method="POST"
|
||||
id="contact-form"
|
||||
<form
|
||||
method="POST"
|
||||
action="?/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
|
||||
content="Your IP will be logged for anti-abuse measures."
|
||||
icon="i-fa6-solid:lock"
|
||||
/>
|
||||
<Meta
|
||||
inputType="email"
|
||||
inputPlaceholder="Your email"
|
||||
selectType="commentType"
|
||||
<option
|
||||
value=""
|
||||
selected
|
||||
disabled>Select a type of comment</option
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
selected
|
||||
disabled>Select a type of comment</option
|
||||
>
|
||||
<option value="Feedback">Feedback</option>
|
||||
<option value="Suggestion">Suggestion</option>
|
||||
<option value="Question">Question</option>
|
||||
<option value="Bug">Bug</option>
|
||||
</Meta>
|
||||
<TextArea
|
||||
id="comment"
|
||||
name="message"
|
||||
placeholder="Your message"
|
||||
/>
|
||||
<Captcha />
|
||||
</Form>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="i-fa6-solid:circle-info" />
|
||||
<span>The contact form is currently disabled.</span>
|
||||
</div>
|
||||
{/if}
|
||||
<option value="Feedback">Feedback</option>
|
||||
<option value="Suggestion">Suggestion</option>
|
||||
<option value="Question">Question</option>
|
||||
<option value="Bug">Bug</option>
|
||||
</Meta>
|
||||
<TextArea
|
||||
id="comment"
|
||||
name="message"
|
||||
placeholder="Your message"
|
||||
/>
|
||||
<Captcha>
|
||||
{#if form?.success}
|
||||
{form.message}
|
||||
{/if}
|
||||
|
||||
{#if form?.error}
|
||||
{form.message}
|
||||
{/if}
|
||||
</Captcha>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
|
@ -1,13 +1,9 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { env } from "$env/dynamic/private";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
export const load: PageServerLoad = async ({ fetch }) => {
|
||||
return {
|
||||
state: await fetch(env.VITE_API_URL + "/api/v1/state/status").then(
|
||||
instances: await fetch("/api/status").then(
|
||||
(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>
|
||||
|
||||
{#if data.state.enabled}
|
||||
<div class="flex flex-col gap-4">
|
||||
<CardOuter>
|
||||
<div class="flex flex-col">
|
||||
{#each data.instances.status as group}
|
||||
<h2>{group.name}</h2>
|
||||
<div class="flex flex-row flex-wrap gap-8">
|
||||
{#each group.data as item}
|
||||
<CardInner
|
||||
title={item.name}
|
||||
description={item.description}
|
||||
icon={item.icon}
|
||||
>
|
||||
<LinksOuter>
|
||||
<div class="flex flex-col gap-4">
|
||||
<CardOuter>
|
||||
<div class="flex flex-col">
|
||||
{#each data.instances.status as group}
|
||||
<h2>{group.name}</h2>
|
||||
<div class="flex flex-row flex-wrap gap-8">
|
||||
{#each group.data as item}
|
||||
<CardInner
|
||||
title={item.name}
|
||||
description={item.description}
|
||||
icon={item.icon}
|
||||
>
|
||||
<LinksOuter>
|
||||
<InstanceLink
|
||||
url={item.link}
|
||||
item={item.status}
|
||||
type="main"
|
||||
/>
|
||||
|
||||
{#if item.us}
|
||||
<InstanceLink
|
||||
url={item.link}
|
||||
item={item.status}
|
||||
type="main"
|
||||
url={item.us}
|
||||
item={item.statusUs}
|
||||
type="us"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if item.us}
|
||||
<InstanceLink
|
||||
url={item.us}
|
||||
item={item.statusUs}
|
||||
type="us"
|
||||
/>
|
||||
{/if}
|
||||
{#if item.bp}
|
||||
<InstanceLink
|
||||
url={item.bp}
|
||||
item={item.statusBp}
|
||||
type="backup"
|
||||
/>
|
||||
{/if}
|
||||
</LinksOuter>
|
||||
</CardInner>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CardOuter>
|
||||
|
||||
{#if item.bp}
|
||||
<InstanceLink
|
||||
url={item.bp}
|
||||
item={item.statusBp}
|
||||
type="backup"
|
||||
/>
|
||||
{/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}
|
||||
<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>
|
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