add segfaultapi functionality and use postgres

This commit is contained in:
Akis 2022-12-30 19:02:49 +02:00
parent daaca19af8
commit a846dd1e2d
Signed by untrusted user: akis
GPG Key ID: 267BF5C6677944ED
48 changed files with 1576 additions and 377 deletions

View File

@ -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
View File

@ -6,4 +6,5 @@ node_modules
.env
.env.*
!.env.example
config/config.yml
package-lock.json

View File

@ -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
View 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"

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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(() => ({}))
};
};

View File

@ -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}

View File

@ -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(() => ({}))
};
};

View File

@ -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(() => ({})),
};
};

View File

@ -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(() => ({})),
};
};

View File

@ -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
};
};

View File

@ -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(() => ({})),
};
};

View File

@ -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
View 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
})
]
})

View File

@ -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}

View File

@ -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
View 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
View 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;

View File

@ -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)
}
}
};

View File

@ -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 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}

View 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");
}
}

View File

@ -0,0 +1,10 @@
<slot />
<style>
:global(.col) {
display: flex;
flex-direction: column;
width: fit-content;
gap: 10px;
}
</style>

View 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>

View 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." };
}
}

View 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>

View 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." };
}
}
}
}
}

View 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>

View 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;

View 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;

View 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"])
}
}
};

View File

@ -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>

View 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)
}
}
};

View 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
}
}
};

View 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
}
}
};

View 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
}
}
};

View 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
}
}
};

View File

@ -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"] });
}
}
}
}

View File

@ -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>

View File

@ -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(() => ({}))
)
};
};

View File

@ -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>

View File

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

View 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}