Compare commits

..

6 Commits

Author SHA1 Message Date
cc4abc85fe Update dependency svelte to v5.25.8 2025-04-08 02:03:45 +00:00
ngn
90af3d0500
update the app_url config option
All checks were successful
Build the docker image for the API / build (push) Successful in 2m6s
Build the docker image for the frontend application / build (push) Successful in 37s
Signed-off-by: ngn <ngn@ngn.tf>
2025-04-08 03:07:14 +03:00
ngn
8d16273540
add /api route to the API documentation
All checks were successful
Build the docker image for the doc server / build (push) Successful in 14s
Signed-off-by: ngn <ngn@ngn.tf>
2025-04-08 03:00:02 +03:00
ngn
4fb78c0ab7
update frontend app version
All checks were successful
Build the docker image for the frontend application / build (push) Successful in 35s
Signed-off-by: ngn <ngn@ngn.tf>
2025-04-08 02:55:58 +03:00
ngn
a57a4955ba
fix internal api url
Signed-off-by: ngn <ngn@ngn.tf>
2025-04-08 02:54:23 +03:00
ngn
ccf0d8abf9
make stuff work without js
All checks were successful
Build the docker image for the API / build (push) Successful in 2m7s
Build the docker image for the frontend application / build (push) Successful in 42s
Signed-off-by: ngn <ngn@ngn.tf>
2025-04-08 02:39:37 +03:00
22 changed files with 111 additions and 128 deletions

View File

@ -26,14 +26,10 @@ jobs:
- name: Build image - name: Build image
run: | run: |
cd app cd app
docker build --build-arg WEBSITE_REPORT_URL=https://git.ngn.tf/ngn/website/issues/new \ docker build --build-arg WEBSITE_REPORT_URL=https://git.ngn.tf/ngn/website/issues/new \
--build-arg WEBSITE_SOURCE_URL=https://git.ngn.tf/ngn/website \ --build-arg WEBSITE_SOURCE_URL=https://git.ngn.tf/ngn/website \
--build-arg WEBSITE_APP_URL_CLEAR=https://ngn.tf \ --build-arg WEBSITE_DOC_URL=http://doc:7003 \
--build-arg WEBSITE_APP_URL_ONION=http://ngntfwmwovvku6eqi7dzzgzv2wzlvq2cqtqha7ccgzub2xnivsuxnuyd.onion \ --build-arg WEBSITE_API_URL=http://api:7002 \
--build-arg WEBSITE_APP_URL_I2P=http://ngn.i2p \ --build-arg WEBSITE_API_PATH=/api \
--build-arg WEBSITE_API_URL_CLEAR=https://api.ngn.tf \
--build-arg WEBSITE_API_URL_ONION=http://api.ngntfwmwovvku6eqi7dzzgzv2wzlvq2cqtqha7ccgzub2xnivsuxnuyd.onion \
--build-arg WEBSITE_API_URL_I2P=https://api.ngn.i2p \
--build-arg WEBSITE_DOC_URL=http://doc:7003 \
--tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest . --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest .
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest

View File

@ -37,14 +37,14 @@ func (c *Type) Load() (err error) {
// default options // default options
c.Options = []Option{ c.Options = []Option{
{Name: "debug", Value: "false", Type: OPTION_TYPE_BOOL, Required: true}, // should display debug messgaes? {Name: "debug", Value: "false", Type: OPTION_TYPE_BOOL, Required: true}, // should display debug messgaes?
{Name: "app_url_clear", Value: "http://localhost:7001/", Type: OPTION_TYPE_URL, Required: true}, // frontend application URL for the website {Name: "app_url", Value: "http://localhost:7001/", Type: OPTION_TYPE_URL, Required: true}, // frontend application URL for the website
{Name: "password", Value: "", Type: OPTION_TYPE_STR, Required: true}, // admin password {Name: "password", Value: "", Type: OPTION_TYPE_STR, Required: true}, // admin password
{Name: "host", Value: "0.0.0.0:7002", Type: OPTION_TYPE_STR, Required: true}, // host the server should listen on {Name: "host", Value: "0.0.0.0:7002", Type: OPTION_TYPE_STR, Required: true}, // host the server should listen on
{Name: "ip_header", Value: "X-Real-IP", Type: OPTION_TYPE_STR, Required: false}, // header that should be checked for obtaining the client IP {Name: "ip_header", Value: "X-Real-IP", Type: OPTION_TYPE_STR, Required: false}, // header that should be checked for obtaining the client IP
{Name: "interval", Value: "1h", Type: OPTION_TYPE_STR, Required: false}, // service status check interval {Name: "interval", Value: "1h", Type: OPTION_TYPE_STR, Required: false}, // service status check interval
{Name: "timeout", Value: "15s", Type: OPTION_TYPE_STR, Required: false}, // timeout for the service status check {Name: "timeout", Value: "15s", Type: OPTION_TYPE_STR, Required: false}, // timeout for the service status check
{Name: "limit", Value: "5s", Type: OPTION_TYPE_STR, Required: false}, // if the service responds slower than this limit, it will be marked as "slow" {Name: "limit", Value: "5s", Type: OPTION_TYPE_STR, Required: false}, // if the service responds slower than this limit, it will be marked as "slow"
} }
c.Count = len(c.Options) c.Count = len(c.Options)

View File

@ -7,7 +7,7 @@ import (
func GET_Index(c *fiber.Ctx) error { func GET_Index(c *fiber.Ctx) error {
conf := c.Locals("config").(*config.Type) conf := c.Locals("config").(*config.Type)
app := conf.GetURL("app_url_clear") app := conf.GetURL("app_url")
// redirect to the API documentation // redirect to the API documentation
return c.Redirect(app.JoinPath("/doc/api").String()) return c.Redirect(app.JoinPath("/doc/api").String())

View File

@ -40,7 +40,7 @@ func GET_News(c *fiber.Ctx) error {
db := c.Locals("database").(*database.Type) db := c.Locals("database").(*database.Type)
conf := c.Locals("config").(*config.Type) conf := c.Locals("config").(*config.Type)
app := conf.GetURL("app_url_clear") app := conf.GetURL("app_url")
lang := c.Params("lang") lang := c.Params("lang")
if lang == "" || len(lang) != 2 { if lang == "" || len(lang) != 2 {

View File

@ -1,32 +1,17 @@
# build the application with node # build the application with node
FROM node:23.11.0 AS build FROM node:23.11.0 AS build
# app URLs
ARG WEBSITE_APP_URL_CLEAR
ARG WEBSITE_APP_URL_ONION
ARG WEBSITE_APP_URL_I2P
ENV WEBSITE_APP_URL_CLEAR=$WEBSITE_APP_URL_CLEAR
ENV WEBSITE_APP_URL_ONION=$WEBSITE_APP_URL_ONION
ENV WEBSITE_APP_URL_I2P=$WEBSITE_APP_URL_I2P
# API URLs
ARG WEBSITE_API_URL_CLEAR
ARG WEBSITE_API_URL_ONION
ARG WEBSITE_API_URL_I2P
ENV WEBSITE_API_URL_CLEAR=$WEBSITE_API_URL_CLEAR
ENV WEBSITE_API_URL_ONION=$WEBSITE_API_URL_ONION
ENV WEBSITE_API_URL_I2P=$WEBSITE_API_URL_I2P
# other config
ARG WEBSITE_REPORT_URL ARG WEBSITE_REPORT_URL
ARG WEBSITE_SOURCE_URL ARG WEBSITE_SOURCE_URL
ARG WEBSITE_DOC_URL ARG WEBSITE_DOC_URL
ARG WEBSITE_API_URL
ARG WEBSITE_API_PATH
ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL
ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL
ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL
ENV WEBSITE_API_URL=$WEBSITE_API_URL
ENV WEBSITE_API_PATH=$WEBSITE_API_PATH
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app

4
app/package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "website", "name": "website",
"version": "6.2", "version": "6.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "website", "name": "website",
"version": "6.2", "version": "6.3",
"dependencies": { "dependencies": {
"dompurify": "^3.2.3", "dompurify": "^3.2.3",
"marked": "^15.0.6", "marked": "^15.0.6",

View File

@ -1,6 +1,6 @@
{ {
"name": "website", "name": "website",
"version": "6.2", "version": "6.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",

View File

@ -1,9 +1,14 @@
import { urljoin, env_url } from "$lib/util.js"; import { browser } from "$app/environment";
import { urljoin } from "$lib/util.js";
const api_version = "v1"; const api_version = "v1";
function api_urljoin(path = null, query = {}) { function api_urljoin(path = null, query = {}) {
let api_url = urljoin(env_url("API"), api_version); let api_url = "";
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(api_url, path, query); return urljoin(api_url, path, query);
} }

View File

@ -1,4 +1,4 @@
import { urljoin, env_url } from "$lib/util.js"; import { urljoin } from "$lib/util.js";
function doc_urljoin(path = null, query = {}) { function doc_urljoin(path = null, query = {}) {
return urljoin(import.meta.env.WEBSITE_DOC_URL, path, query); return urljoin(import.meta.env.WEBSITE_DOC_URL, path, query);

View File

@ -1,14 +1,16 @@
<script> <script>
import { app_url, color, date_from_ts } from "$lib/util.js"; import { color, date_from_ts } from "$lib/util.js";
import { api_get_metrics } from "$lib/api.js"; import { api_get_metrics } from "$lib/api.js";
import Link from "$lib/link.svelte"; import Link from "$lib/link.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
let data = {}; let show_counter = false,
data = {};
onMount(async () => { onMount(async () => {
show_counter = true;
data = await api_get_metrics(fetch); data = await api_get_metrics(fetch);
}); });
</script> </script>
@ -20,24 +22,28 @@
</span> </span>
<span>/</span> <span>/</span>
<span> <span>
<Link link={app_url("/doc/license")} bold={true}>{$_("footer.license")}</Link> <Link link="/doc/license" bold={true}>{$_("footer.license")}</Link>
</span> </span>
<span>/</span> <span>/</span>
<span> <span>
<Link link={app_url("/doc/privacy")} bold={true}>{$_("footer.privacy")}</Link> <Link link="/doc/privacy" bold={true}>{$_("footer.privacy")}</Link>
</span> </span>
</div> </div>
<span class="counter"> {#if show_counter}
{$_("footer.number", { <span class="counter">
values: { {$_("footer.number", {
total: data.total, values: {
since: date_from_ts(data.since), total: data.total,
}, since: date_from_ts(data.since),
})} },
{#if data.number % 1000 == 0} })}
<span style="color: var(--{color()})">({$_("footer.wow")})</span> {#if data.number % 1000 == 0}
{/if} <span style="color: var(--{color()})">({$_("footer.wow")})</span>
</span> {/if}
</span>
{:else}
<span class="counter">{$_("footer.js")}</span>
{/if}
</footer> </footer>
<style> <style>

View File

@ -1,7 +1,5 @@
<script> <script>
import { api_urljoin } from "$lib/api.js"; import { api_urljoin } from "$lib/api.js";
import { app_url } from "$lib/util.js";
export let desc, title; export let desc, title;
</script> </script>
@ -16,7 +14,6 @@
<meta property="og:title" content="[ngn.tf] | {title}" /> <meta property="og:title" content="[ngn.tf] | {title}" />
<meta property="og:description" content={desc} /> <meta property="og:description" content={desc} />
<meta property="og:url" content={app_url()} />
<link <link
rel="alternate" rel="alternate"

View File

@ -1,12 +1,14 @@
<script> <script>
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { color } from "$lib/util.js"; import { color } from "$lib/util.js";
import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
export let picture = ""; export let picture = "";
export let title = ""; export let title = "";
let title_cur = ""; let title_cur = title;
let show_animation = false;
function animate(title) { function animate(title) {
if (!browser) return; if (!browser) return;
@ -24,13 +26,21 @@
} }
} }
onMount(() => {
show_animation = true;
});
$: animate(title); $: animate(title);
</script> </script>
<header> <header>
<div> <div>
<h1 class="title" style="color: var(--{color()})">{title_cur}</h1> {#if show_animation}
<h1 class="cursor" style="color: var(--{color()})">_</h1> <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> </div>
<img src="/profile/{picture}.png" alt="" /> <img src="/profile/{picture}.png" alt="" />
</header> </header>

View File

@ -1,7 +1,9 @@
<script> <script>
import { locale_list, locale_select, locale_index } from "$lib/locale.js"; import { locale_list, locale_select, locale_index } from "$lib/locale.js";
import { onMount } from "svelte";
let len = locale_list.length; let len = locale_list.length;
let show = false;
function get_next(indx) { function get_next(indx) {
let new_indx = 0; let new_indx = 0;
@ -15,11 +17,17 @@
function next() { function next() {
locale_select(get_next($locale_index).code); locale_select(get_next($locale_index).code);
} }
onMount(() => {
show = true;
});
</script> </script>
<button on:click={next}> {#if show}
{get_next($locale_index).icon} <button on:click={next}>
</button> {get_next($locale_index).icon}
</button>
{/if}
<style> <style>
button { button {

View File

@ -1,6 +1,4 @@
import { locale_from_browser } from "$lib/locale.js"; import { locale_from_browser } from "$lib/locale.js";
import { browser } from "$app/environment";
import { page } from "$app/state";
const colors = [ const colors = [
"yellow", "yellow",
@ -25,34 +23,14 @@ function click() {
audio.play(); audio.play();
} }
function urljoin(url, path = null, query = {}) { function urljoin(url, path = null) {
if (undefined === url || null === url) return; if (undefined === url || null === url) return;
let url_len = url.length; if (url[url.length - 1] != "/") url += "/";
if (url[url_len - 1] != "/") url += "/"; if (null === path || "" === path) return url;
if (path[0] === "/") return url + path.slice(1);
if (null === path || "" === path) url = new URL(url); return url + path;
else if (path[0] === "/") url = new URL(path.slice(1), url);
else url = new URL(path, url);
for (let k in query) url.searchParams.append(k, query[k]);
return url.href;
}
function env_url(prefix) {
let host = "";
if (browser) host = window.location.hostname;
if (host.endsWith(".onion")) return import.meta.env["WEBSITE_" + prefix + "_URL_ONION"];
else if (host.endsWith(".i2p")) return import.meta.env["WEBSITE_" + prefix + "_URL_I2P"];
else return import.meta.env["WEBSITE_" + prefix + "_URL_CLEAR"];
}
function app_url(path = null, query = {}) {
return urljoin(env_url("APP"), path, query);
} }
function time_from_ts(ts) { function time_from_ts(ts) {
@ -80,4 +58,4 @@ function date_from_ts(ts) {
}).format(new Date(ts * 1000)); }).format(new Date(ts * 1000));
} }
export { color, click, urljoin, env_url, app_url, time_from_ts, date_from_ts }; export { color, click, urljoin, time_from_ts, date_from_ts };

View File

@ -73,6 +73,7 @@
"license": "License", "license": "License",
"privacy": "Privacy", "privacy": "Privacy",
"number": "Visited {total} times since {since}", "number": "Visited {total} times since {since}",
"wow": "wow!!" "wow": "wow!!",
"js": "Enable javascript to display all the elements"
} }
} }

View File

@ -73,6 +73,7 @@
"license": "Lisans", "license": "Lisans",
"privacy": "Gizlilik", "privacy": "Gizlilik",
"number": "{since} tarihinden beri {total} kez ziyaret edildi", "number": "{since} tarihinden beri {total} kez ziyaret edildi",
"wow": "vay be!!" "wow": "vay be!!",
"js": "Tüm elementleri görüntelemek için javascript'i açın"
} }
} }

View File

@ -7,9 +7,11 @@
import { api_urljoin } from "$lib/api.js"; import { api_urljoin } from "$lib/api.js";
import { locale, _ } from "svelte-i18n"; import { locale, _ } from "svelte-i18n";
import { onMount } from "svelte";
let { data } = $props(); let { data } = $props();
let services = $state(data.services); let services = $state(data.services);
let show_input = $state(false);
function change(input) { function change(input) {
let value = input.target.value.toLowerCase(); let value = input.target.value.toLowerCase();
@ -31,6 +33,10 @@
return s.desc[$locale] !== "" && s.desc[$locale] !== null && s.desc[$locale] !== undefined; return s.desc[$locale] !== "" && s.desc[$locale] !== null && s.desc[$locale] !== undefined;
}); });
} }
onMount(() => {
show_input = true;
});
</script> </script>
<Head title="services" desc="my self-hosted services and projects" /> <Head title="services" desc="my self-hosted services and projects" />
@ -41,7 +47,9 @@
{:else} {:else}
<main> <main>
<div class="title"> <div class="title">
<input oninput={change} type="text" placeholder={$_("services.search")} /> {#if show_input}
<input oninput={change} type="text" placeholder={$_("services.search")} />
{/if}
<div> <div>
<Link icon="nf-fa-feed" link={api_urljoin("/news/" + $locale)}>{$_("services.feed")}</Link> <Link icon="nf-fa-feed" link={api_urljoin("/news/" + $locale)}>{$_("services.feed")}</Link>
</div> </div>

View File

@ -1,2 +1,3 @@
User-Agent: * User-Agent: *
Disallow: /doc/ Disallow: /doc/
Disallow: /api/

View File

@ -24,15 +24,9 @@ const default_env = {
source_url: "https://git.ngn.tf/ngn/website", source_url: "https://git.ngn.tf/ngn/website",
report_url: "https://git.ngn.tf/ngn/website/issues", report_url: "https://git.ngn.tf/ngn/website/issues",
doc_url: "http://localhost:7003", doc_url: "http://localhost:7003",
app_url: { api: {
clear: "http://localhost:7001", url: "http://localhost:7002",
onion: "", path: "http://localhost:7002",
i2p: "",
},
api_url: {
clear: "http://localhost:7002",
onion: "",
i2p: "",
}, },
}; };

View File

@ -5,18 +5,11 @@ services:
build: build:
context: ./app context: ./app
args: args:
# app URLs
WEBSITE_APP_URL_CLEAR: "http://localhost:7001"
WEBSITE_APP_URL_ONION: ""
WEBSITE_APP_URL_I2P: ""
# API URLs
WEBSITE_API_URL_CLEAR: "http://localhost:7002"
WEBSITE_API_URL_ONION: ""
WEBSITE_API_URL_I2P: ""
# other
WEBSITE_SOURCE_URL: "http://github.com/ngn13/website" WEBSITE_SOURCE_URL: "http://github.com/ngn13/website"
WEBSITE_REPORT_URL: "http://github.com/ngn13/website/issues" WEBSITE_REPORT_URL: "http://github.com/ngn13/website/issues"
WEBSITE_DOC_URL: "http://doc:7003" WEBSITE_DOC_URL: "http://doc:7003"
WEBSITE_API_URL: "http://api:7002"
WEBSITE_API_PATH: "http://localhost:7002"
security_opt: security_opt:
- "no-new-privileges:true" - "no-new-privileges:true"
cap_drop: cap_drop:
@ -44,7 +37,7 @@ services:
- ./data.db:/api/data.db:rw - ./data.db:/api/data.db:rw
environment: environment:
WEBSITE_DEBUG: "false" WEBSITE_DEBUG: "false"
WEBSITE_APP_URL_CLEAR: "http://localhost:7001/" WEBSITE_APP_URL: "http://localhost:7001"
WEBSITE_PASSWORD: "change_me" WEBSITE_PASSWORD: "change_me"
WEBSITE_HOST: "0.0.0.0:7002" WEBSITE_HOST: "0.0.0.0:7002"
WEBSITE_IP_HEADER: "X-Real-IP" WEBSITE_IP_HEADER: "X-Real-IP"

View File

@ -1,9 +1,9 @@
My website's API, [api.ngn.tf](https://api.ngn.tf), stores information about my My website's API, stores information about my self-hosted services, it also allows me
self-hosted services, it also allows me to publish news and updates about these to publish news and updates about these services using an Atom feed and it keeps track
services using an Atom feed and it keeps track of visitor metrics. The API itself is of visitor metrics.
written in Go and uses SQLite for storage.
This documentation contains information about all the available API endpoints. This documentation contains information about all the available API endpoints. All the
endpoints can be accessed using the `/api` route.
## Version 1 Endpoints ## Version 1 Endpoints
Each version 1 endpoint, can be accessed using the `/v1` route. Each version 1 endpoint, can be accessed using the `/v1` route.

View File

@ -1,9 +1,9 @@
Websitemin API, [api.ngn.tf](https://api.ngn.tf), self-host edilen servisler hakkında bilgileri Websitemin API, self-host edilen servisler hakkında bilgileri tutuyor, bu servisler hakkında
tutuyor, bu servisler hakkında haberleri ve güncellemeleri bir Atom feed'i aracılığı ile haberleri ve güncellemeleri bir Atom feed'i aracılığı ile paylaşmama izin veriyor ve ziyartçi
paylaşmama izin veriyor ve ziyartçi metriklerini takip ediyor. API'ın kendisi Go ile yazıldı ve metriklerini takip ediyor.
veritabanı olarak SQLite kullanıyor.
Bu dökümentasyon tüm erişeme açık API endpoint'leri hakkında bilgiler içeriyor. 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 ## Versyion 1 Endpoint'leri
Tüm versiyon 1 endpoint'leri `/v1` yolu ile erişilebilir. Tüm versiyon 1 endpoint'leri `/v1` yolu ile erişilebilir.