1 Commits

Author SHA1 Message Date
45ad277d39 Update dependency svelte to v5.35.4 2025-07-07 17:07:00 +00:00
68 changed files with 2885 additions and 2547 deletions

14
.gitignore vendored
View File

@@ -1,7 +1,11 @@
# docker(-compose) stuff
data.db
docker-compose.yaml
docker-compose.yml
compose.yaml
compose.yml
*.yaml
*.yml
*.env
# don't ignore example deployment stuff
!deploy/*
!.github/*
!.gitea/*
!.github/*/*
!.gitea/*/*

View File

@@ -1,4 +1,4 @@
FROM golang:1.24.5
FROM golang:1.24.4
WORKDIR /api

View File

@@ -11,7 +11,4 @@ run:
format:
gofmt -s -w .
clean:
rm -f *.elf
.PHONY: test format clean
.PHONY: test format

View File

@@ -1,6 +1,6 @@
module github.com/ngn13/website/api
go 1.24.5
go 1.24.0
require (
github.com/gofiber/fiber/v2 v2.52.8

View File

@@ -1,5 +1,7 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -13,8 +15,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/ngn13/ortam v0.0.0-20250412195317-e76e62a7a305 h1:1YxtSMwR14PklXNlZxIqcmfpiq2+G98YNmhSuz7GKCQ=
github.com/ngn13/ortam v0.0.0-20250412195317-e76e62a7a305/go.mod h1:MSJZ4ZstrLvVEvivbp9hhup+iL8rvtpgKcYaF3DSOKk=
github.com/ngn13/ortam v0.0.0-20250421004351-8dea81680817 h1:WkHM4w51N5jCsWcDVcPsXz3zhi/kCfNp/VGh2uPjwsk=
github.com/ngn13/ortam v0.0.0-20250421004351-8dea81680817/go.mod h1:MSJZ4ZstrLvVEvivbp9hhup+iL8rvtpgKcYaF3DSOKk=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=

12
app/.gitignore vendored
View File

@@ -1,9 +1,11 @@
.DS_Store
.svelte-kit
node_modules
package
build
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
nerdfonts.*

2
app/.npmrc Normal file
View File

@@ -0,0 +1,2 @@
engine-strict=true
resolution-mode=highest

View File

@@ -3,8 +3,7 @@
"tabWidth": 2,
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 80,
"arrowParens": "avoid",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -1,5 +1,5 @@
# build the application with node
FROM node:24.4.1 AS build
FROM node:23.11.1 AS build
ARG WEBSITE_REPORT_URL
ARG WEBSITE_SOURCE_URL
@@ -16,6 +16,7 @@ ENV WEBSITE_API_PATH=$WEBSITE_API_PATH
WORKDIR /app
COPY . /app
RUN apt install -y make sed wget
RUN npm install
RUN make

View File

@@ -1,7 +1,17 @@
all:
NF_CSS = static/css/nerdfonts.css
NF_WOFF = static/assets/nerdfonts.woff2
all: $(NF_CSS)
npm run build
run:
$(NF_CSS): $(NF_WOFF)
wget "https://www.nerdfonts.com/assets/css/webfont.css" -O $@
sed 's/\.\.\/fonts\/Symbols-2048-em Nerd Font Complete\.woff2/\/assets\/nerdfonts\.woff2/g' -i $@
$(NF_WOFF):
wget "https://www.nerdfonts.com/assets/fonts/Symbols-2048-em%20Nerd%20Font%20Complete.woff2" -O $@
run: $(NF_CSS)
npm run dev
format:
@@ -9,5 +19,7 @@ format:
clean:
rm -rf build
rm $(NF_CSS)
rm $(NF_WOFF)
.PHONY: format run clean

2190
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "website",
"version": "6.5",
"version": "6.3",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -10,18 +10,19 @@
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.2.13",
"@sveltejs/kit": "^2.25.1",
"@sveltejs/vite-plugin-svelte": "^6.1.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.15.1",
"@sveltejs/vite-plugin-svelte": "^4.0.3",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.36.10",
"vite": "^7.0.0"
"prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.16.0",
"vite": "^5.4.11"
},
"type": "module",
"dependencies": {
"dompurify": "^3.2.3",
"js-yaml": "^4.1.0",
"marked": "^16.0.0"
"marked": "^15.0.6",
"svelte-i18n": "^4.0.1"
}
}

View File

@@ -1,57 +0,0 @@
<script>
let { title = "", id = "", children } = $props();
</script>
<main {id}>
{#if title === ""}
<div>{@render children()}</div>
{:else}
<h1>{title}</h1>
<div class="padded">{@render children()}</div>
{/if}
</main>
<style>
main {
display: flex;
flex-direction: column;
height: min-content;
}
main h1 {
font-family: var(--monospace);
font-size: var(--size-6);
color: var(--white-1);
background: var(--black-1);
position: relative;
top: 20px;
right: 7px;
width: min-content;
padding-right: 10px;
}
main h1::before {
color: var(--white-3);
content: "#";
margin: 0 10px 0 0;
}
main div {
color: var(--white-2);
font-size: var(--size-3);
line-height: 1.5em;
word-wrap: break-word;
flex: 1;
align-items: center;
}
main .padded {
padding: 25px 20px 18px 20px;
border: solid 1px var(--black-3);
}
</style>

View File

@@ -1,94 +0,0 @@
<script>
import { _ } from "$lib/locale.js";
import { date, date_from_ts } from "$lib/util.js";
import api from "$lib/api.js";
import { onMount } from "svelte";
let data = $state(null);
onMount(async () => {
data = await api.metrics(fetch);
});
</script>
<footer>
<ul>
<li>
<a href={import.meta.env.WEBSITE_SOURCE_URL}>{$_("footer.source")}</a>
</li>
<li>
<a href="/doc/license">{$_("footer.license")}</a>
</li>
<li>
<a href="/doc/privacy">{$_("footer.privacy")}</a>
</li>
</ul>
{#if data === null}
<span>
{$_("footer.render", {
time: date(new Date()),
})}
</span>
{:else}
<span>
{$_("footer.number", {
total: data.total,
since: date_from_ts(data.since),
})}
{#if data.total % 1000 === 0}
<span class="wow">({$_("footer.wow")})</span>
{/if}
</span>
{/if}
</footer>
<style>
footer {
background: var(--glass);
border-top: solid 2px var(--color);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
padding: 15px 40px;
}
footer ul {
list-style: none;
display: flex;
flex-direction: row;
gap: 5px;
font-size: var(--size-2);
text-align: left;
}
footer ul li a {
text-decoration: none;
font-weight: 200;
}
footer ul li a:hover {
color: var(--color);
}
footer ul li:not(:last-of-type)::after {
content: " / ";
color: var(--white-3);
font-weight: 900;
}
footer span {
color: var(--white-2);
font-size: 15px;
}
footer .wow {
color: var(--color);
}
</style>

View File

@@ -1,32 +0,0 @@
<script>
import { locale } from "$lib/locale.js";
import api from "$lib/api.js";
let { desc, title } = $props();
</script>
<svelte:head>
<title>ngn.tf | {title}</title>
<!-- standart metadata tags -->
<meta name="description" content={desc} />
<meta name="author" content="ngn" />
<meta name="keywords" content="ngn,ngn13,ngn.tf" />
<meta name="color-scheme" content="only dark" />
<meta name="theme-color" content="#000000" />
<!-- open graph meta tags -->
<meta property="og:title" content="ngn.tf | {title}" />
<meta property="og:description" content={desc} />
<!-- atom feed for the service updates -->
<link
rel="alternate"
type="application/atom+xml"
href={api.join("/news/" + $locale.code)}
title="Service news and updates"
/>
</svelte:head>

View File

@@ -1,72 +0,0 @@
<script>
import { _ } from "$lib/locale.js";
let { picture, title } = $props();
</script>
<header>
<div style="--length: {title.length + 1}">
<h1>{title}<span>_</span></h1>
</div>
<img src="/assets/{picture}.png" alt="" />
</header>
<style>
header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: end;
background: var(--transparent);
height: 250px;
}
header div {
padding: 0 40px 10px 40px;
color: var(--color);
font-size: var(--size-6);
font-family:
Consolas,
Monaco,
Lucida Console,
Liberation Mono,
DejaVu Sans Mono,
Bitstream Vera Sans Mono,
Courier New,
monospace;
white-space: nowrap;
}
header div h1 {
text-shadow: var(--text-shadow);
overflow: hidden;
white-space: nowrap;
animation: typing 0.8s steps(var(--length), end);
}
header div span {
content: "_";
display: inline-block;
animation: blink 1.5s steps(2) infinite;
}
header img {
width: 220px;
padding: 0 50px;
}
@media only screen and (max-width: 900px) {
header {
height: 180px;
justify-content: center;
align-items: center;
}
header div {
padding: 30px 20px 20px 20px;
}
header img {
display: none;
}
}
</style>

View File

@@ -1,31 +0,0 @@
<script>
import { localizer, next } from "$lib/locale.js";
/* if we have javascript, no need to send the GET request to the server to
* change the language, we can just switch it on the browser */
function onsubmit(e) {
e.preventDefault();
localizer.switch();
}
</script>
<form method="get" {onsubmit}>
<input type="hidden" name="l" value={$next.code} />
<button type="submit">{$next.icon}</button>
</form>
<style>
button {
cursor: pointer;
background: none;
color: var(--white-1);
font-size: var(--size-4);
outline: none;
border: none;
transition: 0.4s;
}
button:hover {
background: var(--black-1);
}
</style>

View File

@@ -1,46 +0,0 @@
<script>
import Page from "$components/page.svelte";
import Language from "$components/language.svelte";
import { _ } from "$lib/locale.js";
</script>
<nav>
<h3>ngn.tf</h3>
<div>
<Page link="/">{$_("navbar.home")}</Page>
<Page link="/services">{$_("navbar.services")}</Page>
<Page link="/donate">{$_("navbar.donate")}</Page>
<Language />
</div>
</nav>
<style>
nav {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background: var(--glass);
box-shadow: var(--box-shadow);
border-bottom: solid 2px var(--color);
padding: 15px 40px;
}
nav div {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: right;
gap: 15px;
}
nav h3 {
color: var(--color);
font-family: var(--monospace);
font-size: var(--size-4);
font-weight: 900;
}
</style>

View File

@@ -1,35 +0,0 @@
<script>
import { click } from "$lib/util.js";
import { page } from "$app/stores";
let { link, children } = $props();
</script>
<a
class={$page.url.pathname === link ? "active" : "inactive"}
data-sveltekit-preload-data
onclick={click}
href={link}
>
{@render children()}
</a>
<style>
a {
font-weight: 600;
font-size: var(--size-3);
color: var(--white-1);
text-decoration: none;
text-decoration-color: var(--color);
}
a:hover {
color: var(--color);
text-decoration: underline;
}
.active {
color: var(--color);
}
</style>

View File

@@ -1,141 +0,0 @@
<script>
import { time_from_ts } from "$lib/util.js";
import { _, locale } from "$lib/locale.js";
let { service } = $props();
</script>
<main>
<div class="info">
<div>
<h1>{service.name}</h1>
<p>{service.desc[$locale.code]}</p>
</div>
<ul>
<li><a href={service.clear}> Clear</a></li>
<span>|</span>
<li><a href={service.onion}>TOR</a></li>
<span>|</span>
<li><a href={service.i2p}> I2P</a></li>
</ul>
</div>
<div class="check">
<p>
{$_("services.last", {
time: time_from_ts(service.check_time),
})}
</p>
{#if service.check_res == 0}
<span style="background: var(--white-2)">
{$_("services.status.down")}
</span>
{:else if service.check_res == 1}
<span style="background: var(--color)">
{$_("services.status.up")}
</span>
{:else if service.check_res == 2}
<span style="background: var(--color); filter: brightness(50%);">
{$_("services.status.slow")}
</span>
{/if}
</div>
</main>
<style>
main {
display: flex;
flex-direction: column;
background: var(--black-2);
border: solid 1px var(--black-3);
text-align: left;
flex: 1;
}
main .info {
display: flex;
flex-direction: row;
align-items: start;
justify-content: space-between;
flex: 1;
color: var(--white-1);
padding: 15px 18px;
}
main .info div h1 {
font-size: var(--size-4);
font-weight: 900;
}
main .info div p {
font-size: var(--size-2);
color: var(--white-2);
font-weight: 100;
}
main .info ul {
display: flex;
flex-direction: row;
gap: 5px;
text-align: right;
list-style: none;
}
main .info li a {
color: var(--color);
text-decoration: none;
font-size: var(--size-2);
font-weight: 100;
}
main .info li a:hover {
text-decoration: underline;
}
main .info li a[href=""] {
color: var(--white-3);
cursor: not-allowed;
}
main .info li a[href=""]:hover {
text-decoration: none;
text-shadow: none;
}
main .info ul span {
color: var(--white-3);
}
main .check {
border-top: solid 1px var(--black-3);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
color: var(--white-1);
}
main .check p {
font-size: var(--size-2);
font-weight: 100;
color: var(--white-2);
padding: 5px 18px;
word-break: keep-all;
white-space: nowrap;
}
main .check span {
font-size: var(--size-4);
font-weight: 1000;
color: var(--black-1);
padding: 5px 18px;
text-transform: uppercase;
}
</style>

View File

@@ -1,14 +0,0 @@
import { redirect } from "@sveltejs/kit";
/** @type {import('@sveltejs/kit').HandleServerError} */
export async function handleError({ error, status }) {
// for unknown routes, just redirect to /
if (status === 404) {
return redirect(303, "/");
}
// for other errors, pass the message which will be used by the error page
return {
message: `${error}`,
};
}

View File

@@ -1,59 +1,42 @@
import { browser } from "$app/environment";
import { urljoin } from "$lib/util.js";
class API {
constructor() {
this.version = "v1";
}
const api_version = "v1";
// join given path and queries into an API URL
join(path = null, query = {}) {
let base = "";
function api_urljoin(path = null, query = {}) {
let api_url = "";
if (browser) {
base = urljoin(import.meta.env.WEBSITE_API_PATH, this.version);
} else {
base = urljoin(import.meta.env.WEBSITE_API_URL, this.version);
}
if (browser) api_url = urljoin(import.meta.env.WEBSITE_API_PATH, api_version);
else api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version);
return urljoin(base, path, query);
}
// check given JSON body for errors
check_err(json) {
if (!("error" in json))
throw new Error('API response is missing the "error" key');
if (json["error"] != "")
throw new Error(`API returned an error: ${json["error"]}`);
if (!("result" in json))
throw new Error('API response is missing the "result" key');
}
// make a HTTP GET request to the given URL
async GET(fetch, url) {
const res = await fetch(url);
const json = await res.json();
this.check_err(json);
return json["result"];
}
// get visitor metrics
async metrics(fetch) {
return await this.GET(fetch, this.join("/metrics"));
}
// get service list
async services(fetch) {
return await this.GET(fetch, this.join("/services"));
}
// get projects list
async projects(fetch) {
return await this.GET(fetch, this.join("/projects"));
}
return urljoin(api_url, path, query);
}
const api = new API();
export default api;
function api_check_err(json) {
if (!("error" in json)) throw new Error('API response is missing the "error" key');
if (json["error"] != "") throw new Error(`API returned an error: ${json["error"]}`);
if (!("result" in json)) throw new Error('API response is missing the "result" key');
}
async function api_http_get(fetch, url) {
const res = await fetch(url);
const json = await res.json();
api_check_err(json);
return json["result"];
}
async function api_get_metrics(fetch) {
return await api_http_get(fetch, api_urljoin("/metrics"));
}
async function api_get_services(fetch) {
return await api_http_get(fetch, api_urljoin("/services"));
}
async function api_get_projects(fetch) {
return await api_http_get(fetch, api_urljoin("/projects"));
}
export { api_version, api_urljoin, api_get_metrics, api_get_services, api_get_projects };

50
app/src/lib/card.svelte Normal file
View File

@@ -0,0 +1,50 @@
<script>
export let title;
</script>
<main>
<h1 class="title">{title}</h1>
<div>
<slot></slot>
</div>
</main>
<style>
main {
flex: 1;
flex-basis: 30%;
display: flex;
flex-direction: column;
}
main .title {
font-family:
Consolas,
Monaco,
Lucida Console,
Liberation Mono,
DejaVu Sans Mono,
Bitstream Vera Sans Mono,
Courier New,
monospace;
color: var(--white-1);
}
main .title::before {
content: "#";
margin: 0 10px 0 0;
color: var(--white-3);
}
main div {
border-left: solid 1px var(--black-4);
padding: 25px 25px 10px 25px;
font-size: var(--size-4);
color: var(--white-1);
word-wrap: break-word;
align-items: center;
margin-left: 7px;
flex: 1;
}
</style>

View File

@@ -1,36 +1,28 @@
import { urljoin } from "$lib/util.js";
class Doc {
// join given path and queries with a document server URL
join(path = null, query = {}) {
return urljoin(import.meta.env.WEBSITE_DOC_URL, path, query);
}
// check JSON response and throw an error if it contains one
check_err(json) {
if ("error" in json)
throw new Error(`Doc server returned an error: ${json["error"]}`);
}
// send a HTTP request to the documentation server
async GET(fetch, url) {
const res = await fetch(url);
const json = await res.json();
this.check_err(json);
return json;
}
// get a list of all the documentations
async list(fetch) {
return await this.GET(fetch, this.join("/list"));
}
// get a documentation
async get(fetch, name) {
let url = this.join(`/get/${name}`);
return await this.GET(fetch, url);
}
function doc_urljoin(path = null, query = {}) {
return urljoin(import.meta.env.WEBSITE_DOC_URL, path, query);
}
const doc = new Doc();
export default doc;
function doc_check_err(json) {
if ("error" in json) throw new Error(`Documentation server returned an error: ${json["error"]}`);
}
async function doc_http_get(fetch, url) {
const res = await fetch(url);
const json = await res.json();
doc_check_err(json);
return json;
}
async function doc_get_list(fetch) {
return await doc_http_get(fetch, doc_urljoin("/list"));
}
async function doc_get(fetch, name) {
let url = doc_urljoin("/get");
url = urljoin(url, name);
return await doc_http_get(fetch, url);
}
export { doc_urljoin, doc_get, doc_get_list };

62
app/src/lib/error.svelte Normal file
View File

@@ -0,0 +1,62 @@
<script>
import Link from "$lib/link.svelte";
import { color } from "$lib/util.js";
import { _ } from "svelte-i18n";
export let error = "";
</script>
<main>
<h1 style="color: var(--{color()})">{$_("error.title")}</h1>
<code>
{#if error === ""}
Unknown error
{:else}
{error}
{/if}
</code>
<Link link={import.meta.env.WEBSITE_REPORT_URL}>
{$_("error.report")}
</Link>
<img src="/profile/sad.png" alt="" />
</main>
<style>
main {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: end;
align-items: flex-start;
gap: 10px;
padding: 50px;
font-size: var(--size-4);
background: var(--background);
background-size: 50%;
}
main h1 {
font-size: var(--size-6);
}
main code {
font-size: var(--size-4);
color: var(--white-2);
}
main img {
width: var(--profile-size);
position: absolute;
right: 0;
bottom: 0;
}
</style>

83
app/src/lib/footer.svelte Normal file
View File

@@ -0,0 +1,83 @@
<script>
import { color, date_from_ts } from "$lib/util.js";
import { api_get_metrics } from "$lib/api.js";
import Link from "$lib/link.svelte";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
let show_counter = false,
data = {};
onMount(async () => {
show_counter = true;
data = await api_get_metrics(fetch);
});
</script>
<footer style="border-top: solid 2px var(--{color()});">
<div class="links">
<span>
<Link link={import.meta.env.WEBSITE_SOURCE_URL} bold={true}>{$_("footer.source")}</Link>
</span>
<span>/</span>
<span>
<Link link="/doc/license" bold={true}>{$_("footer.license")}</Link>
</span>
<span>/</span>
<span>
<Link link="/doc/privacy" bold={true}>{$_("footer.privacy")}</Link>
</span>
</div>
{#if show_counter}
<span class="counter">
{$_("footer.number", {
values: {
total: data.total,
since: date_from_ts(data.since),
},
})}
{#if data.number % 1000 == 0}
<span style="color: var(--{color()})">({$_("footer.wow")})</span>
{/if}
</span>
{:else}
<span class="counter">{$_("footer.js")}</span>
{/if}
</footer>
<style>
footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background: var(--black-1);
box-sizing: border-box;
padding: 20px 50px 20px 50px;
}
div {
display: flex;
font-size: var(--size-2);
flex-direction: column;
gap: 5px;
}
span {
color: var(--white-2);
font-size: 15px;
}
.counter {
text-align: right;
}
.links {
text-align: left;
display: flex;
flex-direction: row;
gap: 5px;
}
</style>

24
app/src/lib/head.svelte Normal file
View File

@@ -0,0 +1,24 @@
<script>
import { api_urljoin } from "$lib/api.js";
export let desc, title;
</script>
<svelte:head>
<title>[ngn.tf] | {title}</title>
<meta name="description" content={desc} />
<meta name="author" content="ngn" />
<meta name="keywords" content="ngn,ngn13,ngn1,ngn.tf" />
<meta name="color-scheme" content="only dark" />
<meta name="theme-color" content="#000000" />
<meta property="og:title" content="[ngn.tf] | {title}" />
<meta property="og:description" content={desc} />
<link
rel="alternate"
type="application/atom+xml"
href={api_urljoin("/news/en")}
title="Service news and updates"
/>
</svelte:head>

108
app/src/lib/header.svelte Normal file
View File

@@ -0,0 +1,108 @@
<script>
import { browser } from "$app/environment";
import { color } from "$lib/util.js";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
export let picture = "";
export let title = "";
let title_cur = title;
let show_animation = false;
function animate(title) {
if (!browser) return;
let id = window.setTimeout(function () {}, 0);
while (id--) clearTimeout(id);
title_cur = "";
for (let i = 0; i < title.length; i++) {
setTimeout(() => {
title_cur += title[i];
}, i * 70);
}
}
onMount(() => {
show_animation = true;
});
$: animate(title);
</script>
<header>
<div>
{#if show_animation}
<h1 class="title" style="color: var(--{color()})">{title_cur}</h1>
<h1 class="cursor" style="color: var(--{color()})">_</h1>
{:else}
<h1 class="title" style="color: var(--{color()})">{title}</h1>
{/if}
</div>
<img src="/profile/{picture}.png" alt="" />
</header>
<style>
header {
background: var(--background);
background-size: 50%;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: end;
}
header div {
display: flex;
flex-direction: row;
align-items: end;
padding: 50px 50px 30px 50px;
font-size: var(--size-6);
font-family:
Consolas,
Monaco,
Lucida Console,
Liberation Mono,
DejaVu Sans Mono,
Bitstream Vera Sans Mono,
Courier New,
monospace;
white-space: nowrap;
justify-content: start;
width: min-content;
}
header div .title {
text-shadow: var(--text-shadow);
overflow: hidden;
}
header div .cursor {
content: "_";
display: inline-block;
animation: blink 1.5s steps(2) infinite;
}
header img {
padding: 50px 50px 0 50px;
width: var(--profile-size);
bottom: 0;
left: 0;
}
@media only screen and (max-width: 900px) {
header {
display: block;
}
header img {
display: none;
}
}
</style>

6
app/src/lib/icon.svelte Normal file
View File

@@ -0,0 +1,6 @@
<script>
import { color } from "$lib/util.js";
export let icon = "";
</script>
<i style="color: var(--{color()});" class="nf {icon}"></i>

37
app/src/lib/link.svelte Normal file
View File

@@ -0,0 +1,37 @@
<script>
import Icon from "$lib/icon.svelte";
import { color } from "$lib/util.js";
const default_color = "white-1";
export let active = false;
export let highlight = true;
export let link = "";
export let icon = "";
let style = "";
if (highlight) style = `text-decoration-color: var(--${color()});`;
if (active) style += `color: var(--${color()});`;
else style += `color: var(--${default_color});`;
</script>
{#if icon != ""}
<Icon {icon} />
{/if}
{#if highlight}
<a data-sveltekit-preload-data {style} href={link}>
<slot></slot>
</a>
{:else}
<a data-sveltekit-preload-data {style} class="no-highlight" href={link}>
<slot></slot>
</a>
{/if}
<style>
.no-highlight:hover {
text-decoration: none;
}
</style>

View File

@@ -1,225 +1,66 @@
import { derived, get, writable } from "svelte/store";
import { init, locale, register, waitLocale } from "svelte-i18n";
import { browser } from "$app/environment";
import yaml from "js-yaml";
import { get, writable } from "svelte/store";
// defines a single locale
class Locale {
constructor(code, icon) {
// regex for "render"ing the locale
this.ref_regex = /\[[0-9]*:.*?\]/gm;
this.link_regex = /\[[^\]]*]\([^ ]*\)/gm;
this.bold_regex = /\*\*.*?\*\*/gm;
const locale_default = "en";
let locale_index = writable(0);
let locale_list = [];
this.code = code; // BCP 47 language tag
this.icon = icon; // icon for the locale
this.all = {}; // all the locales
}
function locale_setup() {
// english
register("en", () => import("../locales/en.json"));
locale_list.push({ code: "en", name: "English", icon: "🇬🇧" });
// load the locale
async load() {
const text = await import(`../locales/${this.code}.yaml?raw`);
this.all = yaml.load(text.default);
}
// turkish
register("tr", () => import("../locales/tr.json"));
locale_list.push({ code: "tr", name: "Turkish", icon: "🇹🇷" });
// renders a given locale using given values and links
render(locale, values, links) {
// get rid of newlines and remove trailing/repeating spaces and stuff
locale = locale.replaceAll("\r", "").replaceAll("\n", " ");
locale = locale.trim();
// find and replace all the values
for (let name in values) {
// values cannot be objects
if (typeof values[name] === "object") {
continue;
}
// "{name}" will be replaced by the values
locale = locale.replaceAll(`{${name}}`, values[name]);
}
// find all the references
const refs = [...locale.matchAll(this.ref_regex)];
// and repalce them
for (let i = 0; i < refs.length; i++) {
let ref = refs[i][0];
let name = ref.replaceAll(/(^\[|\]$)/g, "");
let indx = parseInt(name.charAt(0)) - 1;
name = name.substring(2);
// check the index
if (indx >= links.length) {
continue;
}
// replace the reference with a link
locale = locale.replaceAll(ref, `<a href="${links[indx]}">${name}</a>`);
}
// look for []() patterns, which are used adding links, kinda like markdown
// but this is shittier
links = [...locale.matchAll(this.link_regex)];
// replace the found links
for (let i = 0; i < links.length; i++) {
let link = links[i][0];
let name = link.match(/(?<=\[).*?(?=])/g);
let url = link.match(/(?<=\]\()[^ ]*(?=\))/g);
// if we fail to extract the link name and/or URL, skip this match
if (null === name || null === url) continue;
locale = locale.replaceAll(link, `<a href="${url[0]}">${name[0]}</a>`);
}
// look for double stars which is used for bold text
const bolds = [...locale.matchAll(this.bold_regex)];
// replace bold text with actual bold text
for (let i = 0; i < bolds.length; i++) {
let bold = bolds[i][0];
let text = bold.match(/(?<=\*\*).*?(?=\*\*)/g);
// if we fail to extract the text content, skip this match
if (null === text) continue;
locale = locale.replaceAll(bold, `<b>${text}</b>`);
}
return locale;
}
// resolve the given locale
resolve(key, vars = {}) {
let cur = this.all;
let keys = key.split(".");
for (let i = 0; i < keys.length; i++) {
cur = cur[keys[i]];
if (cur === undefined) {
return cur;
}
}
// locale needs to be a string
if (typeof cur !== "string") {
return undefined;
}
// extract links from the vars
let links = vars.links;
delete vars.links;
return this.render(cur, vars, links);
}
init({
fallbackLocale: locale_default,
initialLocale: get(locale),
});
}
// localizer stores and defines all the locales
class Localizer {
constructor() {
// list of supported locales
this.list = [
new Locale("en", "🇬🇧󠁧󠁢󠁥󠁮󠁧󠁿"), // English
new Locale("tr", "🇹🇷"), // Turkish
];
this.current = writable(this.list[0]); // current locale
this.next = writable(this.list[1]); // next locale
this.fallback = this.list[0]; // fallback locale
}
// get the name of a language using the current locale
name(code) {
return get(this.current).name(code);
}
// get the current browser locale tag
browser() {
if (browser) {
window.navigator.language.slice(0, 2).toLowerCase();
} else {
return this.fallback.code;
}
}
/* load all the locales, attempt to set the provided locale as the current
* locale, if the provided locale is not available just use the default */
async setup(code) {
for (let i = 0; i < this.list.length; i++) {
await this.list[i].load();
}
// find the locale index by it's code
let indx = this.list.findIndex(locale => {
return locale.code === code;
});
// check the index to see if we found the locale
if (indx < 0) {
return false;
}
// if we actually found it, set it as the current locale
this.current.set(this.list[indx]);
// set the next locale
if (++indx >= this.list.length) {
this.next.set(this.list[0]);
} else {
this.next.set(this.list[indx]);
}
}
// switch to the next locale
switch() {
// find the next locale's index
let indx = this.list.findIndex(locale => {
return locale === get(this.next);
});
// set next locale as the new current locale
this.current.set(get(this.next));
document.cookie = `locale=${get(this.next).code};`;
// get the next locale based on the index
if (indx === this.list.length - 1) {
this.next.set(this.list[(indx = 0)]);
} else {
this.next.set(this.list[++indx]);
}
}
resolve(key, vars = {}) {
// attempt to resolve the given key
let res = get(this.current).resolve(key, vars);
if (res !== undefined) {
return res;
}
// if we fail to resolve the key, try to resolve it using the fallback
// locale, if that fails too then we are kinda fucked so yeah we just throw
// an error
if (get(this.current) === this.fallback) {
throw new Error(`missing key: ${key}`);
} else {
return this.fallback.resolve(key, vars);
}
}
function locale_from_browser() {
if (browser) return window.navigator.language.slice(0, 2).toLowerCase();
else return locale_default;
}
export const localizer = new Localizer(); // global localizer
export const locale = localizer.current; // current locale
export const next = localizer.next; // next locale
function locale_select(l = null) {
if (l === null) {
if (browser && null !== (l = localStorage.getItem("locale"))) locale_select(l);
else locale_select(locale_from_browser());
return;
}
// resolve a given locale by it's key
export const _ = derived(
l = l.slice(0, 2);
for (let i = 0; i < locale_list.length; i++) {
if (l !== locale_list[i].code) continue;
if (browser) localStorage.setItem("locale", l);
locale.set(l);
locale_index.set(i);
return;
}
locale.set(locale_default);
locale_index.set(0);
}
async function locale_wait() {
await waitLocale();
}
export {
locale,
() =>
(key, vars = {}) =>
localizer.resolve(key, vars)
);
export default localizer;
locale_list,
locale_index,
locale_default,
locale_setup,
locale_wait,
locale_select,
locale_from_browser,
};

42
app/src/lib/navbar.svelte Normal file
View File

@@ -0,0 +1,42 @@
<script>
import NavbarLink from "./navbar_link.svelte";
import NavbarSwitch from "./navbar_switch.svelte";
import { color } from "$lib/util.js";
import { _ } from "svelte-i18n";
</script>
<nav style="border-bottom: solid 2px var(--{color()});">
<h3 style="color: var(--{color()})">[ngn.tf]</h3>
<div>
<NavbarLink link="/">{$_("navbar.home")}</NavbarLink>
<NavbarLink link="/services">{$_("navbar.services")}</NavbarLink>
<NavbarLink link="/donate">{$_("navbar.donate")}</NavbarLink>
<NavbarSwitch />
</div>
</nav>
<style>
nav {
box-shadow: var(--box-shadow-1);
background: var(--black-1);
padding: 20px 30px 20px 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
div {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: right;
gap: 15px;
}
h3 {
font-weight: 900;
font-size: var(--size-4);
}
</style>

View File

@@ -0,0 +1,27 @@
<script>
import { color, click } from "$lib/util.js";
import { page } from "$app/stores";
export let link;
function is_active() {
return $page.url.pathname == link;
}
</script>
<a
style="text-decoration-color: var(--{color()}); {is_active() ? `color: var(--${color()})` : ''}"
data-sveltekit-preload-data
on:click={click}
href={link}
>
<slot></slot>
</a>
<style>
a {
font-weight: 900;
font-size: var(--size-4);
color: var(--white-1);
}
</style>

View File

@@ -0,0 +1,45 @@
<script>
import { locale_list, locale_select, locale_index } from "$lib/locale.js";
import { onMount } from "svelte";
let len = locale_list.length;
let show = false;
function get_next(indx) {
let new_indx = 0;
if (indx + 1 >= len) indx = 0;
else new_indx = indx + 1;
return locale_list[new_indx];
}
function next() {
locale_select(get_next($locale_index).code);
}
onMount(() => {
show = true;
});
</script>
{#if show}
<button on:click={next}>
{get_next($locale_index).icon}
</button>
{/if}
<style>
button {
background: var(--black-2);
color: var(--white-1);
font-size: var(--size-4);
outline: none;
border: none;
transition: 0.4s;
}
button:hover {
background: var(--black-1);
}
</style>

114
app/src/lib/service.svelte Normal file
View File

@@ -0,0 +1,114 @@
<script>
import Icon from "$lib/icon.svelte";
import Link from "$lib/link.svelte";
import { color, time_from_ts } from "$lib/util.js";
import { locale, _ } from "svelte-i18n";
export let service = {};
</script>
<main>
<div class="info">
<div class="title">
<h1>{service.name}</h1>
<p>{service.desc[$locale]}</p>
</div>
<div class="links">
<Link highlight={false} link={service.clear}><Icon icon="nf-oct-link" /></Link>
{#if service.onion != ""}
<Link highlight={false} link={service.onion}><Icon icon="nf-linux-tor" /></Link>
{/if}
{#if service.i2p != ""}
<Link highlight={false} link={service.i2p}
><span style="color: var(--{color()})">I2P</span></Link
>
{/if}
</div>
</div>
<div class="check">
<h1>
{$_("services.last", {
values: { time: time_from_ts(service.check_time) },
})}
</h1>
{#if service.check_res == 0}
<span style="background: var(--white-2)">
{$_("services.status.down")}
</span>
{:else if service.check_res == 1}
<span style="background: var(--{color()})">
{$_("services.status.up")}
</span>
{:else if service.check_res == 2}
<span style="background: var(--{color()}); filter: brightness(50%);">
{$_("services.status.slow")}
</span>
{/if}
</div>
</main>
<style>
main {
display: flex;
flex-direction: column;
background: var(--black-3);
border: solid 1px var(--black-4);
text-align: left;
flex: 1;
flex-basis: 40%;
}
main .info {
padding: 25px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
color: var(--white-1);
flex: 1;
}
main .info .title h1 {
font-size: var(--size-5);
margin-bottom: 8px;
font-weight: 900;
}
main .info .title p {
font-size: var(--size-4);
color: var(--white-2);
font-weight: 100;
}
main .info .links {
display: flex;
flex-direction: row;
gap: 10px;
font-size: var(--size-6);
}
main .check {
border-top: solid 1px var(--black-4);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
color: var(--white-1);
}
main .check h1 {
padding: 15px 25px 15px 25px;
font-size: var(--size-4);
font-weight: 100;
}
main .check span {
padding: 15px 25px 15px 25px;
font-size: var(--size-5);
text-transform: uppercase;
color: var(--black-1);
font-weight: 1000;
}
</style>

View File

@@ -1,60 +1,61 @@
import { localizer } from "$lib/locale.js";
import { locale_from_browser } from "$lib/locale.js";
// colors defined in static/css/global.css
const colors = ["yellow", "cyan", "green", "pinkish", "red", "blue"];
const colors = [
"yellow",
"cyan",
"green",
"pinkish",
"red",
// "blue" (looks kinda ass)
];
// randomly select a color
export function color() {
return colors[Math.floor(Math.random() * colors.length)];
let colors_pos = -1;
function color() {
if (colors_pos < 0) colors_pos = Math.floor(Math.random() * colors.length);
else if (colors_pos >= colors.length) colors_pos = 0;
return colors[colors_pos];
}
// play a click sound
export function click() {
function click() {
let audio = new Audio("/assets/click.wav");
audio.play();
}
// join a given path to the URL
export function urljoin(url, path = null) {
if (null === path || path.length === 0) {
return url;
}
function urljoin(url, path = null) {
if (undefined === url || null === url) return;
if (url[url.length - 1] != "/") {
url += "/";
}
if (path[0] === "/") {
path = path.slice(1);
}
if (url[url.length - 1] != "/") url += "/";
if (null === path || "" === path) return url;
if (path[0] === "/") return url + path.slice(1);
return url + path;
}
// convert Date() to readable date
export function date(date) {
return new Intl.DateTimeFormat(localizer.browser(), {
month: "2-digit",
year: "2-digit",
day: "2-digit",
}).format(date);
}
function time_from_ts(ts) {
if (ts === 0 || ts === undefined) return;
// convert timestamp to readable time
export function time_from_ts(ts) {
const date = new Date(ts * 1000);
const zone = date.toString().match(/([A-Z]+[\+-][0-9]+)/)[1];
let ts_date = new Date(ts * 1000);
let ts_zone = ts_date.toString().match(/([A-Z]+[\+-][0-9]+)/)[1];
return (
new Intl.DateTimeFormat(localizer.browser(), {
new Intl.DateTimeFormat(locale_from_browser(), {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(date) + ` (${zone})`
}).format(ts_date) + ` (${ts_zone})`
);
}
// convert timestamp to readable date
export function date_from_ts(ts) {
return date(new Date(ts * 1000));
function date_from_ts(ts) {
if (ts === 0 || ts === undefined) return;
return new Intl.DateTimeFormat(locale_from_browser(), {
month: "2-digit",
year: "2-digit",
day: "2-digit",
}).format(new Date(ts * 1000));
}
export { color, click, urljoin, time_from_ts, date_from_ts };

79
app/src/locales/en.json Normal file
View File

@@ -0,0 +1,79 @@
{
"navbar": {
"home": "home",
"services": "services",
"donate": "donate"
},
"home": {
"title": "hello world!",
"welcome": {
"title": "about",
"desc": "Welcome to my website, I'm ngn",
"whoami": "I'm a security, privacy and freedom advocate high-schooler from Turkey",
"interest": "I'm interested in system security and software development",
"support": "I love and support Free/Libre and Open Source Software (FLOSS)"
},
"work": {
"title": "work",
"desc": "I don't currently have a job, so I spend most of my time...",
"build": "building stupid shit",
"fix": "fixing stupid shit",
"ctf": "solving CTF challenges",
"contribute": "contributing to random projects",
"wiki": "expanding my wiki"
},
"links": {
"title": "contact",
"desc": "Here are some useful links if you want to get in contact with me",
"prefer": "I highly prefer email, you can send encrypted emails using my PGP key"
},
"services": {
"title": "services",
"desc": "A part from working on stupid shit, I host free (as in freedom and price) services available for all",
"speed": "All of these services are available over an 1 Gbit interface",
"security": "All use SSL encrypted connection and they respect your privacy and freedom",
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
"bullshit": "No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit",
"link": "See all the services!"
},
"projects": {
"title": "projects",
"desc": "I mostly work on free software projects, here are some of projects that you might find interesting"
}
},
"services": {
"title": "service status",
"none": "No services found",
"search": "Search for a service",
"feed": "News and updates",
"last": "Last checked at {time}",
"status": {
"up": "Up",
"down": "Down",
"slow": "Slow"
}
},
"donate": {
"title": "donate!",
"info": "I spend a lot of time and money on different projects and maintaining different services.",
"price": "I mostly pay for hosting and electricity. Which when added up costs around 550₺ per month (~$15 at the time of writing).",
"details": "So even a small donation would be useful. And it would help me keep everything up and running.",
"thanks": "Also huge thanks to all of you who have donated so far!",
"table": {
"platform": "Platform",
"address": "Adress/Link"
}
},
"error": {
"title": "something went wrong!",
"report": "Report this issue"
},
"footer": {
"source": "Source",
"license": "License",
"privacy": "Privacy",
"number": "Visited {total} times since {since}",
"wow": "wow!!",
"js": "Enable javascript to display all the elements"
}
}

View File

@@ -1,98 +0,0 @@
navbar:
home: home
services: services
donate: donate
home:
title: hello world!
welcome:
title: about
desc: |
Welcome to my personal website, I'm ngn. You might also know me as ngn13.
I am a security, privacy and freedom advocate high-school student from
Turkey. I'm mainly interesed in software development and system security,
so I usually spend my free time writing [Free/Libre
software](https://www.gnu.org/philosophy/free-sw.html), solving
[CTF](https://en.wikipedia.org/wiki/Capture_the_flag_(cybersecurity))s and
learning new things.
thanks: |
I have few personal software projects and I host web based services. If
you would like to check them out, see the sections below. Thank you for
visiting my website and I hope you have a good rest of your day!
projects:
title: projects
desc: |
I develop and work on Free/Libre software. I don't work on massive
projects and I'm not a professional but I do enjoy it. Here are some of my
more popular projects that you might find interesting
services:
title: services
desc: |
I host web based services. Most of these services provide front-end or
alternatives to already existing popular web services. Here are all the
details
speed: All of these services are available over an 1 Gbit interface
security: All use SSL encrypted connection and they respect your privacy and freedom
blocks: |
Accessible from clearnet, [TOR](https://www.torproject.org/) and
[I2P](https://geti2p.net/en/) - no region or network blocks
bullshit: No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit
link: See all the services!
contact:
title: contact
desc: |
If you have question or a suggestion, or if you simply want to chat, feel
free to send me an email at **ngn [at] ngn.tf**. You can use my [PGP
key](https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D) to
send me an encrypted email, which I would highly prefer. My PGP key also
includes identity claims, which you can use to verify my public profiles.
mastodon: |
I'm also on [mastodon](https://defcon.social/@ngn) where I mainly post
Turkish stuff.
services:
title: service status
desc: |
Here you can find a list of all the publicly accessible services I host. I
have periodic status check for clearnet addresses: A request is sent to the
clearnet address the status of the service is reported based on the
response. The latest reported status is displayed next to the time of the
report. If you experience any issues with a service even though the status
is displayed as up, please [contact me](/#contact) to report this issue.
feed: |
I also have an [1:Atom feed] that you can follow to get the latest news and
updates about the changes made to the different services.
last: Last checked at {time}
status:
up: Up
down: Down
slow: Slow
donate:
title: donate!
desc: |
If you like my work or if you are feeling generous, you can financially
support me. I spend around $15 every month to maintain everything, so even a
small donation would be highly appreciated and it would help me keep
everything up and running.
info: |
Currently I only accept donations with [Monero
(XMR)](https://www.getmonero.org/). Here is my monero address
thanks: |
Also I would like to thank everyone who has donated so far. I really
appreciate it!
error:
title: something went wrong!
report: Report this issue
footer:
source: Source
license: License
privacy: Privacy
number: Visited {total} times since {since}
wow: wow!!
render: Rendered on {time} just for you

79
app/src/locales/tr.json Normal file
View File

@@ -0,0 +1,79 @@
{
"navbar": {
"home": "anasayfa",
"services": "servisler",
"donate": "bağış"
},
"home": {
"title": "merhaba dünya!",
"welcome": {
"title": "hakkımda",
"desc": "Websiteme hoşgeldiniz, ben ngn",
"whoami": "Türkiye'den, güvenlik, gizlik ve özgürlük savunucusu bir liseliyim",
"interest": "Sistem güvenliği ve yazılım geliştirmek ile ilgileniyorum",
"support": "Özgür/Libre ve Açık Kaynaklı Yazılımı (FLOSS) seviyorum ve destekliyorum"
},
"work": {
"title": "iş",
"desc": "Şuan bir işim yok, o yüzden zamanımın çoğunu şunlarla geçiriyorum:",
"build": "salak şeyler inşa etmek",
"fix": "salak şeyleri düzeltmek",
"ctf": "CTF challenge'ları çözmek",
"contribute": "rastgele projelere katkıda bulunmak",
"wiki": "wikimi genişletmek"
},
"links": {
"title": "iletişim",
"desc": "Eğer benim ile iletişime geçmek istiyorsanız, işte bazı faydalı linkler",
"prefer": "Email'i fazlasıyla tercih ediyorum, PGP anahtarım ile şifreli email'ler gönderebilirsiniz"
},
"services": {
"title": "servisler",
"desc": "Salak şeyler inşa etmenin yanı sıra, herkes için kullanıma açık özgür ve ücretsiz servisler host ediyorum",
"speed": "Tüm servisler 1 Gbit ağ arayüzü üzerinden erişilebilir",
"security": "Hepsi SSL şifreli bağlantı kullanıyor ve hepsi gizliliğinize ve özgürlüğünüze saygı gösteriyor",
"privacy": "Açık ağdan, TOR ve I2P'den erişilebilirler, bölge ya da ağ blokları yok",
"bullshit": "CDN yok, cloudflare yok, CAPTCHA yok, analitikler ve diğer saçmalıklar yok",
"link": "Tüm servisleri incele!"
},
"projects": {
"title": "projeler",
"desc": "Çoğunlukla özgür yazılım projeleri üzerinde çalışıyorum, işte ilginç bulabileceğiniz bazı projelerim"
}
},
"services": {
"title": "servis durumu",
"none": "Servis bulunamadı",
"search": "Bir servisi ara",
"feed": "Yenilikler ve güncellemeler",
"last": "Son kontrol zamanı {time}",
"status": {
"up": "Çalışıyor",
"down": "Kapalı",
"slow": "Yavaş"
}
},
"donate": {
"title": "bağış yap!",
"info": "Farklı projeler ve farklı servisleri yönetmek için oldukça zaman ve para harcıyorum.",
"price": "Çoğunlukla hosting ve elektrik için ödeme yapıyorum. Bunlar eklendiği zaman aylık 550₺ civarı bir miktar oluyor (yazdığım sırada ~15$).",
"details": "Bu sebepten küçük bir bağış bile oldukça faydalı olacaktır. Ve herşeyi açık ve çalışmakta tutmama yardımcı olacaktır.",
"thanks": "Ayrıca şuana kadar bağışta bulunan herkese çok teşekkür ederim!",
"table": {
"platform": "Platform",
"address": "Adres/Bağlantı"
}
},
"error": {
"title": "birşeyler yanlış gitti!",
"report": "Bu sorunu raporlayın"
},
"footer": {
"source": "Kaynak",
"license": "Lisans",
"privacy": "Gizlilik",
"number": "{since} tarihinden beri {total} kez ziyaret edildi",
"wow": "vay be!!",
"js": "Tüm elementleri görüntelemek için javascript'i açın"
}
}

View File

@@ -1,103 +0,0 @@
navbar:
home: anasayfa
services: servisler
donate: bağış
home:
title: merhaba dünya!
welcome:
title: hakkımda
desc: |
Kişisel websiteme hoşgeldiniz, ben ngn. Beni ngn13 olarak da
bilebilirsiniz. Türkiye'den güvenlik, gizlilik ve özgürlük savunucusu bir
liseliyim. Genel olarak yazılım geliştirme ve sistem güvenliği ile
ilgileniyorum, o yüzden boş zamanımın çoğunu
[Özgür yazılım](https://www.gnu.org/philosophy/free-sw.tr.html) yazarak,
[CTF](https://wikipedia.org/wiki/Capture_the_flag_(cybersecurity)) çözerek
ve yeni şeyler öğrenerek harcıyorum.
thanks: |
Birkaç kişisel yazılım projem var ve web tabanlı servisler barındırıyorum.
Eğer bunları incelemek isterseniz, aşağıdaki bölümlere bakın. Websitemi
ziyaret ettiğiniz için teşekkür ederim ve size iyi günler dilerim!
projects:
title: projeler
desc: |
Özgür yazılım geliştiriyorum ve üzerinde çalışıyorum. Devasa projelerde
çalışmıyorum ve profesyonel değilim ama hoşuma gidiyor. İşte biraz daha
popüler olan ve ilginç bulabileceğiniz bazı projelerim
services:
title: servisler
desc: |
Web tabanlı servisler host ediyorum. Bu servislerin çoğu zaten mevcut
popüler web servislerine arayüz ya da alternatifler sağlıyor. İşte tüm
detaylar
speed: Tüm servisler 1 Gbit ağ arayüzü üzerinden erişilebilir
security: |
Hepsi SSL şifreli bağlantı kullanıyor ve mahremiyetinize ve özgürlüğünüze
saygı gösteriyor
blocks: |
ık ağdan, [TOR](https://www.torproject.org/tr/) ve
[I2P](https://geti2p.net/tr/)'den erişilebilirler, bölge ya da ağ blokları
yok
bullshit: CDN yok, cloudflare yok, CAPTCHA yok, analitikler ve diğer saçmalıklar yok
link: Tüm servisleri görüntüle!
contact:
title: contact
desc: |
Eğer bir sorunuz ya da öneriniz varsa, ya da basitçe muhabbet etmek
istiyorsanız, bana **ngn [at] ngn.tf** adresinden bir e-posta göndermekten
çekinmeyin. [PGP anahtarımı
](https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D) bana
tercihim olan şifreli e-postalar göndermek için kullanabilirsiniz. PGP
anahtarım aynı zamanda identity claim'leri içeriyor, bunları internete
ık profillerimi doğrulamak için kullanabilirisniz.
mastodon: |
Aynı zamanda [mastodon'dayım](https://defcon.social/@ngn),
çoğunlukla Türkçe olan paylaşımlar yapıyorum.
services:
title: servis durumu
desc: |
Burada barındırdığım internete açık tüm servislerin bir listesini
bulabilirsiniz. Açık ağ adresleri için periyodik bir durum kontrolü mevcut:
ık ağ adresine bir istek gönderiliyor ve servisin durumu verilen cevaba
göre raporlanıyor. En son raporlanan durum raporun zamanının yanında
görüntüleniyor. Bir servisin durumu çalışıyor olarak görünmesine rağmen
servisle alakalı sorunlar yaşıyorsanız, lütfen bu sorunları raporlamak için
[bana ulaşın](/#contact).
feed: |
Farklı servislere yapılan değişiklikler hakkında güncellemeleri ve
haberleri görmek adına takip edebileceğiniz bir [1:Atom beslemesi] de
mevcut.
last: Son kontrol zamanı {time}
status:
up: Çalışıyor
down: Kapalı
slow: Yavaş
donate:
title: bağış yap!
desc: |
Eğer emeğimi seviyorsanız ya da kendinizi cömert hissediyorsanız, beni
finansal olarak destekleyebilirsiniz. Herşeyi yönetmek adına her yaklaşık
$15 harcıyorum, o yüzden küçük bir bağış bile oldukça faydalı olacaktır ve
herşeyi ayakta tutmama yardım eder.
info: |
Şuan sadece [Monero (XMR)](https://www.getmonero.org/tr/) ile bağış kabul
ediyorum. İşte monero adresim
thanks: Ayrıca şuana kadar bağış yapan herkese çok teşekkür ederim.
error:
title: birşeyler yanlış gitti!
report: Bu hatayı raporla
footer:
source: Kaynak
license: Lisans
privacy: Gizlilik
number: "{since} tarihinden beri {total} kez ziyaret edildi"
wow: vay be!
render: Sizin için {time} tarihinde işlendi

View File

@@ -1,51 +1,8 @@
<script>
import { page } from "$app/state";
import { _ } from "$lib/locale.js";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
onMount(() => {
goto("/");
});
</script>
<main>
<h1>{$_("error.title")}</h1>
<code>{page.error.message}</code>
<a href={import.meta.env.WEBSITE_REPORT_URL}>{$_("error.report")}</a>
<img src="/assets/sad.png" alt="" />
</main>
<style>
main {
background: var(--transparent);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: end;
align-items: flex-start;
gap: 10px;
padding: 50px;
font-size: var(--size-4);
}
main h1 {
font-size: var(--size-6);
text-shadow: var(--text-shadow);
color: var(--color);
}
main code {
font-size: var(--size-4);
color: var(--white-2);
}
main img {
width: 250px;
position: absolute;
right: 50px;
bottom: 0;
}
</style>

View File

@@ -1,6 +1,6 @@
import localizer from "$lib/locale.js";
import { locale_setup, locale_wait } from "$lib/locale.js";
export async function load({ data }) {
await localizer.setup(data.locale);
return data;
export async function load() {
locale_setup();
await locale_wait();
}

View File

@@ -1,36 +0,0 @@
import { redirect } from "@sveltejs/kit";
import { color } from "../lib/util.js";
// set the locale cookie
function set_locale(cookies, locale) {
cookies.set("locale", locale, { path: "/", secure: false, httpOnly: false });
}
export async function load({ cookies, request, url }) {
// if the locale param is specified, use the specified locale
let locale = url.searchParams.get("l");
if (locale) {
set_locale(cookies, locale);
return redirect(307, url.pathname);
}
// attempt get the preferred locale from cookies
locale = cookies.get("locale");
/* if that doesn't work, try the accept-language header, and update the
* cookies respectively */
if (!locale) {
locale = request.headers.get("accept-language")?.split(",")[0];
set_locale(cookies, locale);
}
return {
// the locale we detected
locale: locale,
/* color is randomly picked on the server and passed via data to the client
* so both the client and the server will have the same color */
color: color(),
};
}

View File

@@ -1,22 +1,23 @@
<script>
import { browser } from "$app/environment";
import Navbar from "$lib/navbar.svelte";
import Footer from "$lib/footer.svelte";
import { color } from "$lib/util.js";
import api from "$lib/api.js";
import { locale_select } from "$lib/locale.js";
import { onMount } from "svelte";
let { data, children } = $props();
let { children } = $props();
if (browser) {
// set the current app and the API version
window._version = {
app: pkg.version,
api: api.version,
};
}
onMount(() => {
locale_select();
});
</script>
<main style="--color: var(--{data.color})">
{@render children()}
<main>
<Navbar />
<div class="content">
{@render children()}
</div>
<Footer />
</main>
<style>
@@ -27,4 +28,9 @@
flex-direction: column;
min-height: 100vh;
}
.content {
background: var(--black-1);
flex-grow: 1;
}
</style>

View File

@@ -1,9 +1,15 @@
import api from "$lib/api.js";
import { api_get_projects } from "$lib/api.js";
export async function load({ fetch }) {
let projects = await api.projects(fetch);
return {
projects: null === projects ? [] : projects,
};
try {
let projects = await api_get_projects(fetch);
return {
projects: null === projects ? [] : projects,
error: "",
};
} catch (err) {
return {
error: err.toString(),
};
}
}

View File

@@ -1,94 +1,129 @@
<script>
import Navbar from "$components/navbar.svelte";
import Footer from "$components/footer.svelte";
import Header from "$components/header.svelte";
import Head from "$components/head.svelte";
import Card from "$components/card.svelte";
import { api_version } from "$lib/api.js";
import Header from "$lib/header.svelte";
import Error from "$lib/error.svelte";
import Head from "$lib/head.svelte";
import Card from "$lib/card.svelte";
import Link from "$lib/link.svelte";
import { locale, _ } from "$lib/locale.js";
import { browser } from "$app/environment";
import { _, locale } from "svelte-i18n";
import { color } from "$lib/util.js";
let { data } = $props();
// return list of projects that have decriptions for the given locale
function projects() {
return data.projects.filter(p => {
return (
p.desc[$locale.code] !== "" &&
p.desc[$locale.code] !== null &&
p.desc[$locale.code] !== undefined
);
});
if (browser) {
window._version = {};
window._version.app = pkg.version;
window._version.api = api_version;
}
</script>
<Head title="home" desc="home page of my personal website" />
<Navbar />
<Header picture="tired" title={$_("home.title")} />
<main>
<!-- welcome -->
<Card>
<p>{@html $_("home.welcome.desc")}</p>
<br />
<p>{$_("home.welcome.thanks")}</p>
</Card>
<!-- projects -->
<Card title={$_("home.projects.title")} id="projects">
<p>{$_("home.projects.desc")}:</p>
<br />
<ul>
{#each projects() as project}
<li class="project">
<a href={project.url}>{project.name}</a>: {project.desc[$locale.code]}
{#if data.error.length !== 0}
<Error error={data.error} />
{:else}
<main>
<Card title={$_("home.welcome.title")}>
<span> 👋 {$_("home.welcome.desc")}</span>
<ul>
<li>🇹🇷 {$_("home.welcome.whoami")}</li>
<li>🖥️ {$_("home.welcome.interest")}</li>
<li>❤️ {$_("home.welcome.support")}</li>
</ul>
</Card>
<Card title={$_("home.work.title")}>
<span>{$_("home.work.desc")}</span>
<ul>
<li>⌨️ {$_("home.work.build")}</li>
<li>🤦 {$_("home.work.fix")}</li>
<li>🚩 {$_("home.work.ctf")}</li>
<li>👥 {$_("home.work.contribute")}</li>
<li>📑 {$_("home.work.wiki")}</li>
</ul>
</Card>
<Card title={$_("home.links.title")}>
<span>{$_("home.links.desc")}:</span>
<ul>
<li>
<Link
icon="nf-fa-key"
link="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D"
>
PGP
</Link>
</li>
{/each}
</ul>
</Card>
<!-- services -->
<Card title={$_("home.services.title")} id="services">
<p>{$_("home.services.desc")}:</p>
<br />
<ul>
<li>{@html $_("home.services.speed")}</li>
<li>{@html $_("home.services.security")}</li>
<li>{@html $_("home.services.blocks")}</li>
<li>{@html $_("home.services.bullshit")}</li>
</ul>
<br />
<a class="services" href="/services" data-sveltekit-preload-data>
{$_("home.services.link")}
</a>
</Card>
<!-- contact -->
<Card title={$_("home.contact.title")} id="contact">
<p>{@html $_("home.contact.desc")}</p>
<br />
<p>{@html $_("home.contact.mastodon")}</p>
</Card>
</main>
<Footer />
<li>
<Link icon="nf-md-email" link="mailto:ngn@ngn.tf">Email</Link>
</li>
<li>
<Link icon="nf-md-mastodon" link="https://defcon.social/@ngn">Mastodon</Link>
</li>
</ul>
<span>
{$_("home.links.prefer")}
</span>
</Card>
<Card title={$_("home.services.title")}>
<span>
{$_("home.services.desc")}:
</span>
<ul>
<li>
<i style="color: var(--{color()});" class="nf nf-md-speedometer_slow"></i>
{$_("home.services.speed")}
</li>
<li>
<i style="color: var(--{color()});" class="nf nf-fa-lock"></i>
{$_("home.services.security")}
</li>
<li>
<i style="color: var(--{color()});" class="nf nf-fa-network_wired"></i>
{$_("home.services.privacy")}
</li>
<li>
<i style="color: var(--{color()});" class="nf nf-md-eye_off"></i>
{$_("home.services.bullshit")}
</li>
</ul>
<Link link="/services">{$_("home.services.link")}</Link>
</Card>
<Card title={$_("home.projects.title")}>
<span>
{$_("home.projects.desc")}:
</span>
{#if data.error.length === 0}
<ul>
{#each data.projects.filter((p) => {
return p.desc[$locale] !== "" && p.desc[$locale] !== null && p.desc[$locale] !== undefined;
}) as project}
<li>
<Link active={true} link={project.url}>{project.name}</Link>:
{project.desc[$locale]}
</li>
{/each}
</ul>
{/if}
</Card>
</main>
{/if}
<style>
main {
background: var(--black-1);
flex: 1;
display: flex;
flex-direction: column;
flex-wrap: wrap;
justify-content: center;
align-items: stretch;
padding: 40px;
padding: 50px;
gap: 28px;
}
.services {
color: var(--color);
text-decoration-color: var(--color);
}
.project a {
color: var(--color);
text-decoration-color: var(--color);
@media only screen and (max-width: 900px) {
main {
flex-direction: column;
}
}
</style>

View File

@@ -1,7 +1,12 @@
import doc from "$lib/doc";
import { doc_get } from "$lib/doc";
export async function load({ fetch, params }) {
return {
doc: await doc.get(fetch, params.name),
};
try {
return {
doc: await doc_get(fetch, params.name),
error: "",
};
} catch (err) {
return { error: err.toString() };
}
}

View File

@@ -1,41 +1,50 @@
<script>
import Navbar from "$components/navbar.svelte";
import Footer from "$components/footer.svelte";
import Header from "$components/header.svelte";
import Head from "$components/head.svelte";
import { locale, _ } from "$lib/locale.js";
import Header from "$lib/header.svelte";
import Error from "$lib/error.svelte";
import Head from "$lib/head.svelte";
import { locale, _ } from "svelte-i18n";
import { goto } from "$app/navigation";
import { color } from "$lib/util.js";
import DOMPurify from "dompurify";
import { marked } from "marked";
import { onMount } from "svelte";
import { marked } from "marked";
let { data } = $props();
marked.use();
marked.use({ breaks: true });
onMount(() => {
for (let key in data.doc) {
onMount(async () => {
for (let key in data.doc)
data.doc[key]["content"] = DOMPurify.sanitize(data.doc[key]["content"]);
}
if (undefined !== data.error && data.error.includes("not found")) goto("/");
});
</script>
<Head title="documentation" desc="website and API documentation" />
<Navbar />
<Header picture="reader" title={data.doc[$locale.code].title} />
<main>
<div class="markdown-body">
{@html marked.parse(data.doc[$locale.code].content)}
</div>
</main>
<Footer />
<Header picture="reader" title={data.doc[$locale].title} />
{#if data.error.length !== 0}
{#if !data.error.includes("not found")}
<Error error={data.error} />
{/if}
{:else}
<main>
<div class="markdown-body" style="--link-color: var(--{color()})">
{@html marked.parse(data.doc[$locale].content)}
</div>
</main>
{/if}
<style>
@import "/css/markdown.css";
main {
background: var(--black-1);
padding: 40px;
padding: 50px;
gap: 30px;
}
main .markdown-body :global(a) {
color: var(--link-color);
}
</style>

View File

@@ -1,43 +1,93 @@
<script>
import Navbar from "$components/navbar.svelte";
import Footer from "$components/footer.svelte";
import Header from "$components/header.svelte";
import Head from "$components/head.svelte";
import Card from "$components/card.svelte";
import Header from "$lib/header.svelte";
import Head from "$lib/head.svelte";
import Icon from "$lib/icon.svelte";
import { _ } from "$lib/locale.js";
import { color } from "$lib/util.js";
import { _ } from "svelte-i18n";
</script>
<Head title="donate" desc="give me all of your life savings" />
<Navbar />
<Header picture="money" title={$_("donate.title")} />
<main>
<Card>
<p>{@html $_("donate.desc")}</p>
<br />
<p>{@html $_("donate.info")}:</p>
<div>
<code>
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
</code>
</div>
<p>{$_("donate.thanks")}</p>
</Card>
<span> </span>
<span>
{$_("donate.info")}
{$_("donate.price")}
</span>
<br />
<br />
<span>
{$_("donate.details")}
</span>
<table>
<thead>
<tr>
<th style="color: var(--{color()})">{$_("donate.table.platform")}</th>
<th style="color: var(--{color()})">{$_("donate.table.address")}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<Icon icon="nf-fa-monero" />
Monero (XMR)
</td>
<td>
<code>
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
</code>
</td>
</tr>
</tbody>
</table>
<span>
{$_("donate.thanks")}
</span>
</main>
<Footer />
<style>
main {
background: var(--black-1);
flex: 1;
padding: 40px;
padding: 50px;
}
main div {
background: var(--black-2);
border: solid 1px var(--black-3);
padding: 6px;
margin: 15px 0;
main span {
font-size: var(--size-4);
color: var(--white-1);
}
table {
box-shadow: var(--box-shadow);
background: var(--black-3);
border-collapse: collapse;
font-size: var(--size-3);
margin: 30px 0 30px 0;
width: 100%;
}
tr,
th,
td {
color: white;
background: var(--dark-two);
text-align: left;
}
td,
th {
font-size: var(--size-4);
border: solid 1px var(--black-4);
padding: 16px;
}
th {
font-weight: 1000;
}
td {
color: var(--white-2);
font-weight: 400;
}
code {

View File

@@ -1,9 +1,15 @@
import api from "$lib/api.js";
import { api_get_services } from "$lib/api.js";
export async function load({ fetch }) {
let services = await api.services(fetch);
return {
services: null === services ? [] : services,
};
try {
let services = await api_get_services(fetch);
return {
services: null === services ? [] : services,
error: "",
};
} catch (err) {
return {
error: err.toString(),
};
}
}

View File

@@ -1,68 +1,100 @@
<script>
import Navbar from "$components/navbar.svelte";
import Footer from "$components/footer.svelte";
import Header from "$components/header.svelte";
import Service from "$components/service.svelte";
import Head from "$components/head.svelte";
import Card from "$components/card.svelte";
import Service from "$lib/service.svelte";
import Header from "$lib/header.svelte";
import Error from "$lib/error.svelte";
import Link from "$lib/link.svelte";
import Head from "$lib/head.svelte";
import { api_urljoin } from "$lib/api.js";
import { locale, _ } from "svelte-i18n";
import { onMount } from "svelte";
import { locale, _ } from "$lib/locale.js";
import api from "$lib/api.js";
let { data } = $props();
let services = $state(data.services);
let show_input = $state(false);
// filtered list of services that have descriptions for the current locale
let services = $derived(
data.services.filter(
s =>
s.desc[$locale.code] !== "" &&
s.desc[$locale.code] !== null &&
s.desc[$locale.code] !== undefined
)
);
function change(input) {
let value = input.target.value.toLowerCase();
services = [];
if (value === "") {
services = data.services;
return;
}
data.services.forEach((s) => {
if (s.name.toLowerCase().includes(value)) services.push(s);
else if (s.desc[$locale].toLowerCase().includes(value)) services.push(s);
});
}
function get_services() {
return services.filter((s) => {
return s.desc[$locale] !== "" && s.desc[$locale] !== null && s.desc[$locale] !== undefined;
});
}
onMount(() => {
show_input = true;
});
</script>
<Head title="services" desc="my self-hosted services and projects" />
<Navbar />
<Header picture="cool" title={$_("services.title")} />
<main>
<Card>
<p>{@html $_("services.desc")}</p>
<br />
<p>
{@html $_("services.feed", {
links: [api.join("/news/" + $locale.code)],
})}
</p>
</Card>
<div class="services">
{#if services.length === 0}
<h3>{$_("services.none")}</h3>
{:else}
{#each services as service}
<Service {service} />
{/each}
{/if}
</div>
</main>
<Footer />
{#if data.error.length !== 0}
<Error error={data.error} />
{:else}
<main>
<div class="title">
{#if show_input}
<input oninput={change} type="text" placeholder={$_("services.search")} />
{/if}
<div>
<Link icon="nf-fa-feed" link={api_urljoin("/news/" + $locale)}>{$_("services.feed")}</Link>
</div>
</div>
<div class="services">
{#if get_services().length == 0}
<h3 class="none">{$_("services.none")}</h3>
{:else}
{#each get_services() as service}
<Service {service} />
{/each}
{/if}
</div>
</main>
{/if}
<style>
main {
background: var(--black-1);
flex: 1;
padding: 40px;
padding: 50px;
text-align: right;
}
main .title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
main .none {
color: var(--white-3);
}
main .services {
border-top: solid 1px var(--white-3);
padding-top: 20px;
margin-top: 15px;
margin-top: 20px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: stretch;
color: var(--white-3);
gap: 15px;
gap: 28px;
}
@media only screen and (max-width: 1200px) {
main .services {
flex-direction: column;
}
}
</style>

Binary file not shown.

View File

@@ -0,0 +1,11 @@
@keyframes blink {
0% {
opacity: 0;
}
}
@keyframes cursor {
to {
border-color: transparent;
}
}

7
app/static/css/font.css Normal file
View File

@@ -0,0 +1,7 @@
@font-face {
font-family: "Ubuntu";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("/assets/ubuntu.woff2") format("woff2");
}

View File

@@ -1,26 +1,32 @@
/* global CSS file, imported in every page, defines commonly used color, effects
* etc. also defines styles for commonly used elements */
/*
* animations.css: stuff like the cursor animation
* webfont.css: webfont dumped from https://www.nerdfonts.com/assets/css/webfont.css
* font.css: the main font (Ubuntu)
*/
@import "./animations.css";
@import "./nerdfonts.css";
@import "./font.css";
:root {
/* randomly selected colors */
--yellow: #d3b910;
--cyan: #0dd2e8;
--green: #06e00a;
--pinkish: #d506e0;
--red: #e8180d;
--blue: #3768fc;
--blue: #2036f9;
/* white tones */
--white-1: #ffffff;
--white-2: #bfbfbf;
--white-3: #5f5f5f;
--white-4: #0f0f0f;
/* black tones */
--black-1: #000000;
--black-2: #111;
--black-3: #3a3b3c;
--black-2: #050505;
--black-3: #111111;
--black-4: #3a3b3c;
/* different sizes */
--size-1: 8px;
--size-2: 16px;
--size-3: 18px;
@@ -28,39 +34,14 @@
--size-5: 24px;
--size-6: 30px;
/* shadows */
--box-shadow: rgb(38, 57, 77) 0px 20px 30px -10px;
--text-shadow: 0 1px 5px rgba(255, 255, 255, 0.15);
--box-shadow-1: rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px;
--box-shadow-2: rgba(0, 0, 0, 0.35) 0px 30px 60px -12px inset,
rgba(20, 20, 20, 0.3) 0px 18px 36px -18px inset;
/* backgrounds */
--transparent: linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96));
--glass: rgba(1, 1, 1, 0.93);
/* fonts */
--monospace:
ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono,
monospace;
--sans-serif:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
--emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
/* cursor blink animation */
@keyframes blink {
0% {
opacity: 0;
}
}
/* typing animation */
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
--text-shadow: 3px 2px 8px rgba(50, 50, 50, 0.8);
--background: linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96)),
url("/assets/banner.png");
--profile-size: 220px;
}
* {
@@ -73,14 +54,11 @@ html {
}
body {
font-family: var(--sans-serif);
background-image: url("/assets/banner.png");
background: var(--black-1);
font-family: "Ubuntu", sans-serif;
overflow-x: hidden;
height: 100vh;
width: 100vw;
line-height: 1.5;
font-size: var(--size-3);
}
::selection {
@@ -103,24 +81,36 @@ body {
}
a {
color: var(--white-2);
font-weight: 600;
font-weight: 900;
color: var(--white-1);
text-decoration: none;
cursor: pointer;
text-decoration: underline;
text-decoration-color: var(--white-2);
}
a:hover {
color: var(--white-1);
text-decoration: underline;
text-shadow: var(--text-shadow);
}
i .nf {
font-weight: 900;
}
ul {
list-style: inside;
margin: 12px 0 12px 0;
}
br {
display: block;
margin: 5px 0;
li + li {
margin-top: 10px;
}
input {
background: var(--black-3);
border: none;
outline: none;
font-size: var(--size-4);
padding: 10px;
border: solid 1px var(--black-4);
color: var(--white-1);
}

View File

@@ -1,21 +1,15 @@
/* modified version of github-markdown-css, the dark version
* licensed under MIT, see: https://github.com/sindresorhus/github-markdown-css
*/
.markdown-body {
color-scheme: dark;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: #c9d1d9;
background-color: #000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
color: var(--white-2);
}
.markdown-body br {
margin: 0;
}
.markdown-body .octicon {
@@ -66,6 +60,10 @@
display: none !important;
}
.markdown-body a {
text-decoration: none;
}
.markdown-body abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
@@ -83,9 +81,9 @@
.markdown-body h1 {
margin: 0.67em 0;
font-weight: 600;
padding-bottom: 5px;
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid var(--black-3);
border-bottom: 1px solid #21262d;
}
.markdown-body mark {
@@ -288,18 +286,17 @@
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
color: var(--white-1);
margin-top: 30px;
margin-bottom: 10px;
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h2 {
font-weight: 600;
padding-bottom: 5px;
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid var(--black-3);
border-bottom: 1px solid #21262d;
}
.markdown-body h3 {
@@ -361,14 +358,28 @@
.markdown-body tt,
.markdown-body code,
.markdown-body samp {
font-family: var(--monospace);
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font-family: var(--monospace);
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 12px;
word-wrap: normal;
}
@@ -709,7 +720,7 @@
margin: 0;
font-size: 85%;
white-space: break-spaces;
background: var(--black-2);
background: var(--black-3);
border-radius: 6px;
}
@@ -750,13 +761,11 @@
.markdown-body .highlight pre,
.markdown-body pre {
padding: 10px;
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--black-2);
border: solid 1px var(--black-3);
background-color: var(--black-3);
}
.markdown-body pre code,
@@ -970,7 +979,7 @@
.markdown-body g-emoji {
display: inline-block;
min-width: 1ch;
font-family: var(--emoji);
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1em;
font-style: normal !important;
font-weight: 400;
@@ -1017,9 +1026,7 @@
}
.markdown-body .contains-task-list:hover .task-list-item-convert-container,
.markdown-body
.contains-task-list:focus-within
.task-list-item-convert-container {
.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {
display: block;
width: auto;
height: 24px;

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,13 +1,14 @@
//import adapter from '@sveltejs/adapter-auto';
import adapter from "@sveltejs/adapter-node";
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
alias: { $components: "src/components" },
},
compilerOptions: {
runes: true,
onwarn: (warning, handler) => {
if (warning.code === "a11y-click-events-have-key-events") return;
handler(warning);
},
};

View File

@@ -22,7 +22,7 @@ function env_from(prefix, object) {
const default_env = {
source_url: "https://git.ngn.tf/ngn/website",
report_url: "mailto:ngn@ngn.tf",
report_url: "https://git.ngn.tf/ngn/website/issues",
doc_url: "http://localhost:7003",
api: {
url: "http://localhost:7002",

View File

@@ -30,7 +30,6 @@ lint:
clang-tidy --warnings-as-errors --config= $(CSRCS) $(HSRCS)
clean:
rm -f *.elf
rm -rf $(DISTDIR)
run:

View File

@@ -1,64 +1,52 @@
My website's API, stores information about my self-hosted services, it also
allows me to publish news and updates about these services using an Atom feed
and it keeps track of visitor metrics.
My website's API, stores information about my self-hosted services, it also allows me
to publish news and updates about these services using an Atom feed and it keeps track
of visitor metrics.
This documentation contains information about all the available API endpoints.
All the endpoints can be accessed using the `/api` route.
This documentation contains information about all the available API endpoints. All the
endpoints can be accessed using the `/api` route.
## Version 1 Endpoints
Each version 1 endpoint, can be accessed using the `/v1` route.
All the endpoints return JSON formatted data.
### Errors
If any error occurs, you will get a non-200 response. And the JSON data will
have an `error` key, which will contain information about the error that
occured, in the string format. This is the only JSON key that will be set in
non-200 responses.
If any error occurs, you will get a non-200 response. And the JSON data will have an
`error` key, which will contain information about the error that occured, in the
string format. This is the only JSON key that will be set in non-200 responses.
### Results
If no error occurs, `error` key will be set to an emtpy string (""). If the
endpoint returns any data, this will be stored using the `result` key. The
`result` have a different expected type and a format for each endpoint.
If no error occurs, `error` key will be set to an emtpy string (""). If the endpoint
returns any data, this will be stored using the `result` key. The `result` have a
different expected type and a format for each endpoint.
### Multilang
Some `result` formats may use a structure called "Multilang". This is a simple
JSON structure that includes one key for each supported language. The key is
named after the language it represents. Currently only supported languages are:
Some `result` formats may use a structure called "Multilang". This is a simple JSON
structure that includes one key for each supported language. The key is named after
the language it represents. Currently only supported languages are:
- English (`en`)
- Turkish (`tr`)
So each multilang structure, will have **at least** one of these keys.
Here is an example multilang structure:
```
{
"en": "Hello, world!",
"tr": "Merhaba, dünya!"
}
```
If a `result` field is using a multilang structure, it will be specified as
"Multilang" in the rest of the documentation.
If a `result` field is using a multilang structure, it will be specified as "Multilang"
in the rest of the documentation.
### Administrator routes
The endpoints under the `/v1/admin` route, are administrator-only routes. To
access these routes you'll need to specfiy a password using the `Authorization`
header. If the password you specify, matches with the password specified using
the `API_PASSWORD` environment variable, you will be able to access the route.
The endpoints under the `/v1/admin` route, are administrator-only routes. To access
these routes you'll need to specfiy a password using the `Authorization` header. If
the password you specify, matches with the password specified using the `API_PASSWORD`
environment variable, you will be able to access the route.
### GET /v1/services
Returns a list of available services. Each service has the following JSON
format:
Returns a list of available services. Each service has the following JSON format:
```
{
"name": "Test Service",
@@ -74,91 +62,73 @@ format:
"i2p": ""
}
```
Where:
- `name`: Service name (string)
- `desc`: Service description (Multilang)
- `check_time`: Last time status check time for the service, set 0 if status
checking is not supported for this service/status checking is disabled
(integer, UNIX timestamp)
- `check_time`: Last time status check time for the service, set 0 if status checking is
not supported for this service/status checking is disabled (integer, UNIX timestamp)
- `check_res`: Last service status check result (integer)
- 0 if the service is down
- 1 if the service is up
- 2 if the service is up, but slow
- 3 if the service doesn't support status checking/status checking is disabled
* 0 if the service is down
* 1 if the service is up
* 2 if the service is up, but slow
* 3 if the service doesn't support status checking/status checking is disabled
- `check_url`: URL used for service's status check (string, empty if none)
- `clear`: Clearnet URL for the service (string, empty string if none)
- `onion`: Onion (TOR) URL for the service (string, empty string if none)
- `i2p`: I2P URL for the service (string, empty string if none)
You can also get information about a specific service by specifying it's name
using a URL query named `name`.
You can also get information about a specific service by specifying it's name using
a URL query named `name`.
### GET /v1/news/:language
Returns a Atom feed of news for the given language. Supports languages that are
supported by Multilang.
Returns a Atom feed of news for the given language. Supports languages that are supported
by Multilang.
### GET /v1/metrics
Returns metrics about the API usage. The metric data has the following format:
```
{
"since": 1736294400,
"total": 8
"since":1736294400,
"total":8
}
```
Where:
- `since`: Metric collection start date (integer, UNIX timestamp)
- `total`: Total number of visitors (integer)
### GET /v1/admin/logs
Returns a list of administrator logs. Each log has the following JSON format:
```
{
"action": "Added service \"Test Service\"",
"time": 1735861794
}
```
Where:
- `action`: Action that the administrator performed (string)
- `time`: Time when the administrator action was performed (integer, UNIX
timestamp)
- `time`: Time when the administrator action was performed (integer, UNIX timestamp)
### PUT /v1/admin/service/add
Creates a new service. The request body needs to contain JSON data, and it needs
to have the JSON format used to represent a service. See `/v1/services/all`
route to see this format.
Creates a new service. The request body needs to contain JSON data, and it needs to
have the JSON format used to represent a service. See `/v1/services/all` route to
see this format.
Returns no data on success.
### DELETE /v1/admin/service/del
Deletes a service. The client needs to specify the name of the service to
delete, by setting the URL query `name`.
Deletes a service. The client needs to specify the name of the service to delete, by
setting the URL query `name`.
Returns no data on success.
### GET /v1/admin/service/check
Forces a status check for all the services.
Returns no data on success.
### PUT /v1/admin/news/add
Creates a news post. The request body needs to contain JSON data, and it needs
to use the following JSON format:
```
{
"id": "test_news",
@@ -173,9 +143,7 @@ to use the following JSON format:
}
}
```
Where:
- `id`: Unique ID for the news post (string)
- `title`: Title for the news post (Multilang)
- `author`: Author of the news post (string)
@@ -184,8 +152,7 @@ Where:
Returns no data on success.
### DELETE /v1/admin/news/del
Deletes a news post. The client needs to specify the ID of the news post to
delete, by setting the URL query `id`.
Deletes a news post. The client needs to specify the ID of the news post to delete,
by setting the URL query `id`.
Returns no data on success.

View File

@@ -1,64 +1,53 @@
Websitemin API, self-host edilen servisler hakkında bilgileri tutuyor, bu
servisler hakkında haberleri ve güncellemeleri bir Atom feed'i aracılığı ile
paylaşmama izin veriyor ve ziyartçi metriklerini takip ediyor.
Websitemin API, self-host edilen servisler hakkında bilgileri tutuyor, bu servisler hakkında
haberleri ve güncellemeleri bir Atom feed'i aracılığı ile paylaşmama izin veriyor ve ziyartçi
metriklerini takip ediyor.
Bu dökümentasyon tüm erişime açık API endpoint'leri hakkında bilgiler içeriyor.
Tüm endpoint'lere `/api` yolu ile erişilebilir.
Bu dökümentasyon tüm erişime açık API endpoint'leri hakkında bilgiler içeriyor. Tüm endpoint'lere
`/api` yolu ile erişilebilir.
## Versyion 1 Endpoint'leri
Tüm versiyon 1 endpoint'leri `/v1` yolu ile erişilebilir.
Tüm endpoint'ler JSON ile formatlanmış veri döndürür.
### Hatalar
Herhangi bir hata ortaya çıkarsa 200 dışı bir cevap alırsınız. Ve JSON verisinde
bir `error` girdisi bulunur, bu ortaya çıkan hata hakkında gerekli bilgileri,
metin formunda içerir. 200 dışı bir cevap aldığınızda tek JSON girdisi bu
olacaktır.
bir `error` girdisi bulunur, bu ortaya çıkan hata hakkında gerekli bilgileri, metin formunda
içerir. 200 dışı bir cevap aldığınızda tek JSON girdisi bu olacaktır.
### Sonuçlar
Eğer bir hata ortaya çıkmaz ise, `error` girdisi boş bir metin olarak ayarlanır
(""). Eğer endpoint herhangi bir veri döndürüyorsa, bu veri `result` giridisi
ile sağlanır. Her endpoint için `result` girdisinin tipi farklı olabilir.
Eğer bir hata ortaya çıkmaz ise, `error` girdisi boş bir metin olarak ayarlanır ("").
Eğer endpoint herhangi bir veri döndürüyorsa, bu veri `result` giridisi ile sağlanır.
Her endpoint için `result` girdisinin tipi farklı olabilir.
### Multilang
Bazı `result` formatları "Multilang" isimli bir yapıyı kullanabilir. Bu her
desteklenen dil için bir girdi bulunduran basit bir JSON yapısıdır. Her girdi
ifade ettiği dil ile isimlendirilir. Şuan tek desteklenen diller:
Bazı `result` formatları "Multilang" isimli bir yapıyı kullanabilir. Bu her desteklenen
dil için bir girdi bulunduran basit bir JSON yapısıdır. Her girdi ifade ettiği dil
ile isimlendirilir. Şuan tek desteklenen diller:
- English (`en`)
- Turkish (`tr`)
Yani her multilang yapısında, bu girdilerden **en az** bir tanesi bulunur.
İşte örnek bir multilang yapısı:
```
{
"en": "Hello, world!",
"tr": "Merhaba, dünya!"
}
```
Bu dökümantasyonun geri kalanında, eğer bir `result` girdisi bir multilang
yapısı kullanıyorsa, "Multilang" olarak isimlendirilecek.
Bu dökümantasyonun geri kalanında, eğer bir `result` girdisi bir multilang yapısı kullanıyorsa,
"Multilang" olarak isimlendirilecek.
### Yönetici yolları
`/v1/admin` yolu altındaki endpoint'ler yöneticiye-özeldir. Bu yollara erişmek
için, `Authorization` header'ı aracılığı ile bir parola belirtmeniz
gerekecektir. Eğer belritiğiniz parola `API_PASSWORD` ortam değişkeni ile
belirtilen parola ile uyuşuyorsa, yola erişebilirsiniz.
`/v1/admin` yolu altındaki endpoint'ler yöneticiye-özeldir. Bu yollara erişmek için,
`Authorization` header'ı aracılığı ile bir parola belirtmeniz gerekecektir. Eğer
belritiğiniz parola `API_PASSWORD` ortam değişkeni ile belirtilen parola ile
uyuşuyorsa, yola erişebilirsiniz.
### GET /v1/services
Erişilebilir servislerin bir listesini döndürür. Her servis şu JSON formatını
takip eder:
```
{
"name": "Test Service",
@@ -74,93 +63,74 @@ takip eder:
"i2p": ""
}
```
Burada:
- `name`: Servis ismi (metin)
- `desc`: Servis açıklaması (Multilang)
- `check_time`: Servisin en son durumunun kontrol edildiği zaman, eğer bu servis
için durum kontrolü desteklenmiyorsa/durum kontrolü devra dışı bırakılmış ise
0 olarak ayarlanır (sayı, UNIX zaman damgası)
- `check_time`: Servisin en son durumunun kontrol edildiği zaman, eğer bu servis için
durum kontrolü desteklenmiyorsa/durum kontrolü devra dışı bırakılmış ise 0 olarak
ayarlanır (sayı, UNIX zaman damgası)
- `check_res`: En son servis durum kontrolünün sonucu (sayı)
- servis kapalı ise 0
- servis çalışıyor ise 1
- serivs çalışıyor, ama yavaş ise 2
- bu servis için durum kontrolü desteklenmiyorsa/durum kontrolü devre dışı ise
3
- `check_url`: Servis durum kontrolü için kullanılan URL (metin, yoksa boş
metin)
* servis kapalı ise 0
* servis çalışıyor ise 1
* serivs çalışıyor, ama yavaş ise 2
* bu servis için durum kontrolü desteklenmiyorsa/durum kontrolü devre dışı ise 3
- `check_url`: Servis durum kontrolü için kullanılan URL (metin, yoksa boş metin)
- `clear`: Servisin açık ağ URL'si (metin, yoksa boş metin)
- `onion`: Servisin Onion (TOR) URL'si (metin, yoksa boş metin)
- `i2p`: Servisin I2P URL'si (metin, yoksa boş metin)
`name` isimli bir URL sorgusu ile servisin ismini belirterek, spesifik bir
servis hakkında bilgi de alabilirsiniz.
`name` isimli bir URL sorgusu ile servisin ismini belirterek, spesifik bir servis hakkında
bilgi de alabilirsiniz.
### GET /v1/news/:language
Verilen dil için haberlerin bir Atom feed'i döndürür. Multilang tarafından
desteklenen dilleri destekler.
Verilen dil için haberlerin bir Atom feed'i döndürür. Multilang tarafından desteklenen
dilleri destekler.
### GET /v1/metrics
API kullanımı hakkınadaki metrikleri döndürür. Metrik şu formatı kullanır:
```
{
"since": 1736294400,
"total": 8
"since":1736294400,
"total":8
}
```
Burada:
- `since`: Metrik toplama başlangıç tarihi (sayı, UNIX zaman damgası)
- `total`: Toplam ziyaretçi sayısı (sayı)
### GET /v1/admin/logs
Yönetici kayıtlarının bir listesini döndürür. Her kayıt şu JSON formatını takip
eder:
Yönetici kayıtlarının bir listesini döndürür. Her kayıt şu JSON formatını takip eder:
```
{
"action": "Added service \"Test Service\"",
"time": 1735861794
}
```
Burada:
- `action`: Yöneticinin yaptığı eylem (metin)
- `time`: Yönetici eylemin yapıldığı zaman (sayı, UNIX zaman damgası)
### PUT /v1/admin/service/add
Yeni bir servis oluşturur. İstek gövdesinin servis için kullanılan JSON
formatını takip eden JSON verisini içermesi gerekir. Bu formatı görmek için
`/v1/services/all` yoluna bakınız.
Yeni bir servis oluşturur. İstek gövdesinin servis için kullanılan JSON formatını
takip eden JSON verisini içermesi gerekir. Bu formatı görmek için `/v1/services/all`
yoluna bakınız.
Başarılı ise herhangi bir veri döndürmez.
### DELETE /v1/admin/service/del
Bir servisi siler. İstemcinin `name` URL sorgusu ile silinecek servisin ismini
belirtmesi gerekir.
Bir servisi siler. İstemcinin `name` URL sorgusu ile silinecek servisin ismini belirtmesi
gerekir.
Başarılı ise herhangi bir veri döndürmez.
### GET /v1/admin/service/check
Tüm servisler için bir durum kontrolünü zorlar.
Başarılı ise herhangi bir veri döndürmez.
### PUT /v1/admin/news/add
Yeni bir haber paylaşımı oluşturur. İstek gövedisinin JSOn verisi içermesi ve
verilen formatı takip etmesi gerekir:
Yeni bir haber paylaşımı oluşturur. İstek gövedisinin JSOn verisi içermesi ve verilen formatı
takip etmesi gerekir:
```
{
"id": "test_news",
@@ -175,9 +145,7 @@ verilen formatı takip etmesi gerekir:
}
}
```
Burada:
- `id`: Haber paylaşımının benzersiz ID'si (metin)
- `title`: Haber paylaşımının başlığı (Multilang)
- `author`: Haber paylaşımının yazarı (metin)
@@ -186,8 +154,7 @@ Burada:
Başarılı ise herhangi bir veri döndürmez.
### DELETE /v1/admin/news/del
Bir haber paylaşımı siler. İstemcinin `id` URL sorgusu ile silinecek paylaşımın
ID'sini belirtmesi gerekir.
Bir haber paylaşımı siler. İstemcinin `id` URL sorgusu ile silinecek paylaşımın ID'sini
belirtmesi gerekir.
Başarılı ise herhangi bir veri döndürmez.

View File

@@ -1,44 +1,31 @@
As a privacy advocate myself I do my best to respect your privacy while
simultaneously keeping my server safe. I also think it's important to be
transparent about this kind of stuff so I wrote this document to tell you how
exactly I process or store your information.
As a privacy advocate myself I do my best to respect your privacy while simultaneously keeping my server safe. I also think it's
important to be transparent about this kind of stuff so I wrote this document to tell you how exactly I process or store your
information.
## DNS & SSL
Currently I use cloudflare's name servers, however cloudflare doesn't own my
domain (I didn't purchase the domain from cloudflare) nor it proxies any of the
traffic. All of my records use the _DNS only_" mode, meaning they are just DNS
records and they point to my server, not cloudflare. This also means cloudflare
doesn't control any of the SSL certificates. All the certificates are stored on
my server and they are created with Let's Encrypt. So I own the certificates and
it's not possible for cloudlfare to suddenly switch DNS records (it would break
SSL).
Currently I use cloudflare's name servers, however cloudflare doesn't own my domain (I didn't purchase the domain from cloudflare)
nor it proxies any of the traffic. All of my records use the *DNS only*" mode, meaning they are just DNS records and they point
to my server, not cloudflare. This also means cloudflare doesn't control any of the SSL certificates. All the certificates are
stored on my server and they are created with Let's Encrypt. So I own the certificates and it's not possible for cloudlfare to
suddenly switch DNS records (it would break SSL).
## Usage metrics
I don't have any kind of request or traffic monitoring on my server. So no, your HTTP(S) requests or other network
connections are not processed to generate colorful graphs, pie charts and stuff.
I don't have any kind of request or traffic monitoring on my server. So no, your
HTTP(S) requests or other network connections are not processed to generate
colorful graphs, pie charts and stuff.
You may have realized that in the bottom of this page there is total visitor number tracker. It is the only
usage/visitor metric tracking I have, which is implemented by website's API, which is free (as in freedom) so you can
go audit it's code yourself.
You may have realized that in the bottom of this page there is total visitor
number tracker. It is the only usage/visitor metric tracking I have, which is
implemented by website's API, which is free (as in freedom) so you can go audit
it's code yourself.
I want to make it clear that this metric tracker does not store any information
about you or your HTTP(S) requests to a database. It temporarily stores your IP
address' SHA1 hash, in memory, so it doesn't recount the same visitor again when
they refresh the website or visit it multiple times in a short period of time.
After a certain amount of requests, your IP address' SHA1 hash will be removed
from the memory and it will be replaced with a new visitor's SHA1 hash instead.
I want to make it clear that this metric tracker does not store any information about you or your HTTP(S) requests to
a database. It temporarily stores your IP address' SHA1 hash, in memory, so it doesn't recount the same visitor again
when they refresh the website or visit it multiple times in a short period of time. After a certain amount of requests,
your IP address' SHA1 hash will be removed from the memory and it will be replaced with a new visitor's SHA1 hash instead.
## Logs
All the HTTP(S) services are proxied with nginx, and nginx logs all of them to a
file on the disk. This file (`access.log`) is only readable by the root user,
and it is contents are deleted every 4 hours (with `shred` to make sure it
doesn't leave anything on the disk). The logs include _only_ the following
information:
All the HTTP(S) services are proxied with nginx, and nginx logs all of them to a file on the disk. This file (`access.log`)
is only readable by the root user, and it is contents are deleted every 4 hours (with `shred` to make sure it doesn't leave
anything on the disk). The logs include *only* the following information:
- Request time
- Requested host
@@ -46,12 +33,9 @@ information:
- HTTP request method
- HTTP response code
This is the minimal information I need to trace any issues if something goes
wrong, which is the main reason why I use logging at all, to make it easier to
find any issues.
This is the minimal information I need to trace any issues if something goes wrong, which is the main reason why I use
logging at all, to make it easier to find any issues.
## Data removal
If you want to remove any of your data from my server,
[you can contact me](/#contact). And yes this includes removing usage metrics
and logs.
If you want to remove any of your data from my server, [you can send me an email](mailto:ngn@ngn.tf). And yes this includes
removing usage metrics and logs.

View File

@@ -1,46 +1,30 @@
Kişisel olarak ben de bir gizlik savunucusu olduğumdan, bir yandan sunucumu
güvende tutarken bir yandan da gizliliğinize önem göstermek için elimden geleni
yapıyorum. Aynı zamanda bu tarz şeyler hakkında şeffaf ve açık olmanın önemli
olduğunu düşünüyorum, o yüzden verilerinizi nasıl işlediğimi ya da depoladığımı
anlamanız için bu dökümanı yazmaya karar verdim.
Kişisel olarak ben de bir gizlik savunucusu olduğumdan, bir yandan sunucumu güvende tutarken bir yandan da gizliliğinize önem
göstermek için elimden geleni yapıyorum. Aynı zamanda bu tarz şeyler hakkında şeffaf ve açık olmanın önemli olduğunu düşünüyorum,
o yüzden verilerinizi nasıl işlediğimi ya da depoladığımı anlamanız için bu dökümanı yazmaya karar verdim.
## DNS & SSL
Şuan cloudflare'in isim sunucularını kullanıyorum, ancak cloudflare alan adıma
sahip değil (alan adımı cloudflare'den almadım) ve aynı şekilde herhangi bir
trafiğe vekillik etmiyor. Tüm DNS kayıtlarım _Sadece DNS_ modunu kullanıyor,
yani sadece DNS kayıtlarından ibaretler ve benim sunucuma işaret ediyorlar,
cloudflare'e değil. Bu aynı zamanda cloudflare SSL sertifikalarımı kontrol
etmiyor demek. Tüm sertifikalar benim sunucumda tutuluyor ve Let's Encrypt ile
oluşturuldular. Yani sertifikalar bana ait ve cloudflare'in aniden DNS
kayıtlarını değiştirmesi mümkün değil (bu SSL'in bozulmasına sebep olur).
Şuan cloudflare'in isim sunucularını kullanıyorum, ancak cloudflare alan adıma sahip değil (alan adımı cloudflare'den almadım)
ve aynı şekilde herhangi bir trafiğe vekillik etmiyor. Tüm DNS kayıtlarım *Sadece DNS* modunu kullanıyor, yani sadece
DNS kayıtlarından ibaretler ve benim sunucuma işaret ediyorlar, cloudflare'e değil. Bu aynı zamanda cloudflare SSL sertifikalarımı
kontrol etmiyor demek. Tüm sertifikalar benim sunucumda tutuluyor ve Let's Encrypt ile oluşturuldular. Yani sertifikalar bana ait
ve cloudflare'in aniden DNS kayıtlarını değiştirmesi mümkün değil (bu SSL'in bozulmasına sebep olur).
## Kullanım metrikleri
Sunucumda herhangi bir istek ya da trafik monitörlermesi yok. Yani hayır, HTTP(S) istekleriniz ya da diğer ağ
bağlantılarınız renki grafikler, pasta grafikleri gibi şeyler üretmek için işlenmiyor.
Sunucumda herhangi bir istek ya da trafik monitörlermesi yok. Yani hayır,
HTTP(S) istekleriniz ya da diğer ağ bağlantılarınız renki grafikler, pasta
grafikleri gibi şeyler üretmek için işlenmiyor.
Bu sayfanın altında bir ziyaretçi sayısı takipçisi olduğunu farketmiş olabilirsiniz. Bu kullandığım tek kullanım/ziyaretçi
metrik takibi ve websitemin, özgür olan, bu yüzden kodunu kendiniz denetleyebileceğiniz API'ı ile implemente edildi.
Bu sayfanın altında bir ziyaretçi sayısı takipçisi olduğunu farketmiş
olabilirsiniz. Bu kullandığım tek kullanım/ziyaretçi metrik takibi ve
websitemin, özgür olan, bu yüzden kodunu kendiniz denetleyebileceğiniz API'ı ile
implemente edildi.
Bu metrik takipçisinin, HTTP(S) istekleriniz hakkında herhangi bir veriyi bir
veri tabanına kaydetmediğini belirtmek isterim. Bu takipçi geçici olarak IP
adresinizin SHA1 hash'ini bellekte tutuyor, bunun amacı aynı ziyaretçiyi sayfayı
yenilediği zaman ya da kısa bir süre için websitesini birden fazla kez ziyaret
ettiği zaman tekrar saymayı önlemek. Belirli bir miktar istekten sonra, IP
adresinizin SHA1 hash'i bellekten kaldırılacaktır ve yeni bir ziyaretçinin
SHA1'i onun yerine geçicektir.
Bu metrik takipçisinin, HTTP(S) istekleriniz hakkında herhangi bir veriyi bir veri tabanına kaydetmediğini belirtmek isterim.
Bu takipçi geçici olarak IP adresinizin SHA1 hash'ini bellekte tutuyor, bunun amacı aynı ziyaretçiyi sayfayı yenilediği zaman
ya da kısa bir süre için websitesini birden fazla kez ziyaret ettiği zaman tekrar saymayı önlemek. Belirli bir miktar istekten
sonra, IP adresinizin SHA1 hash'i bellekten kaldırılacaktır ve yeni bir ziyaretçinin SHA1'i onun yerine geçicektir.
## Kayıtlar
Tüm HTTP(S) servisleri nginx ile vekilleniyor, ve nginx hepsini disk üzerindeki
bir dosyaya kaydediyor. Bu dosya (`access.log`) sadece root kullanıcısı
tarafından okunabilir, ve içerği her 4 saatde bir siliniyor (diskde veri
kalmadığından emin olmak için shred komutu ile). Kayıtlar _sadece_ aşağıdaki
bilgileri içeriyor:
Tüm HTTP(S) servisleri nginx ile vekilleniyor, ve nginx hepsini disk üzerindeki bir dosyaya kaydediyor. Bu dosya (`access.log`)
sadece root kullanıcısı tarafından okunabilir, ve içerği her 4 saatde bir siliniyor (diskde veri kalmadığından emin olmak için
shred komutu ile). Kayıtlar *sadece* aşağıdaki bilgileri içeriyor:
- İstek zamanı
- İstenilen host
@@ -48,12 +32,9 @@ bilgileri içeriyor:
- HTTP istek yöntemi
- HTTP cevap kodu
Bu birşeyler yanlış giderse sorunları bulmak için ihtiyacım olan en az bilgi,
kayıt tutmamın ana sebeplerinden bir tanesi zaten bu, sorunları bulmayı
kolaylaştırmak.
Bu birşeyler yanlış giderse sorunları bulmak için ihtiyacım olan en az bilgi, kayıt tutmamın ana sebeplerinden bir tanesi
zaten bu, sorunları bulmayı kolaylaştırmak.
## Veri silimi
Sunucumdan herhangi bir verinizi kaldırmak isterseniz,
[bana bir ulaşabilirsiniz](/#contact). Ve evet buna kullanım metrikleri ve
kayıtlar dahil.
Sunucumdan herhangi bir verinizi kaldırmak isterseniz, [bana bir email gönderebilirsiniz](mailto:ngn@ngn.tf). Ve evet buna
kullanım metrikleri ve kayıtlar dahil.