Compare commits
214 Commits
fc11748e57
...
renovate/s
Author | SHA1 | Date | |
---|---|---|---|
5abbbf6677 | |||
b447c8cf13 | |||
e212d43bf2 | |||
2bf94c0f3c | |||
3f49d8294a | |||
323b76fe98 | |||
f714bc6eaa | |||
8ad2778dd0 | |||
0d99a7da69 | |||
c049f6c44a | |||
9acbe212ad | |||
aa29cca4f4 | |||
afc0a30660 | |||
647634addc | |||
dba65911a6 | |||
1067d0a3fd | |||
1ffd51b641 | |||
cd97df5f21 | |||
12dedf3700 | |||
ade745cdc5 | |||
ab5977eaae | |||
39813d246f | |||
975626fc5c | |||
1f47d59760 | |||
38abe411a8 | |||
e7216d51e3 | |||
2b244645c6 | |||
43589f7ddc | |||
b43a219fb3 | |||
7bf92f9049 | |||
577195cb7f | |||
fc64b742bb | |||
922eaeb124 | |||
65ec5a51aa | |||
8036b0c1e1 | |||
c505698b0b
|
|||
4e08375748
|
|||
912bf616b6
|
|||
d1ddbf2605 | |||
66b01bd58d | |||
01468670e3 | |||
1f09aae11e | |||
ce1ac7b09e | |||
f9faddf660 | |||
945f1b84f2 | |||
a9d2633107
|
|||
506d16e0c9
|
|||
30c3b0c37b | |||
2da4f81a6b | |||
cc482fc0ad | |||
51ea22fbc5 | |||
58595d5eb3 | |||
671d44017f | |||
95d24b734b | |||
c2e31fb440 | |||
2d3dab5646 | |||
4b2e12b374 | |||
c6323cae38 | |||
e05d42a956 | |||
426c474016 | |||
91ca908980 | |||
5a61508359 | |||
86c0e3cc97 | |||
1afa33f28b | |||
a22b1e67c8 | |||
f477b4525b | |||
462d35ea0d | |||
693c94274a | |||
af2e5b49ae | |||
33a31dac32 | |||
00b7227d4d | |||
2f9d38cd45 | |||
e68dd1d317 | |||
fe834d2778 | |||
686f51dcde | |||
dc43bdb3af
|
|||
05185cb82a
|
|||
ee3fa10096 | |||
0e4457d5fb | |||
b95c51193e | |||
322d8d5c39 | |||
904e9f6d15 | |||
cc4abc85fe | |||
73cfcea923 | |||
90af3d0500
|
|||
8d16273540
|
|||
4fb78c0ab7
|
|||
a57a4955ba
|
|||
ccf0d8abf9
|
|||
b312e40204
|
|||
a67bd74ced
|
|||
9a72d3f95d
|
|||
66c96ae312
|
|||
f779a03ae5
|
|||
bcb48d789b
|
|||
aa69525912
|
|||
0134bd2ab1
|
|||
84e477b23e | |||
515dcf6a09 | |||
a50cc1ff7f | |||
f2ec9479b1 | |||
e1eae3c05b | |||
53ab201ab3 | |||
c9792fb6be | |||
68808a14f9 | |||
4343192a2c | |||
20fb3cd685 | |||
906ace188b | |||
f2a1a21a8c | |||
9353631931 | |||
4eb5ad6ef3
|
|||
bac570615e
|
|||
037a4405b9
|
|||
389a9d3df4 | |||
921fe4255d | |||
f7e4442de5 | |||
e4d1fc21fb | |||
fe6b3512fd | |||
3dbc2e0510
|
|||
26362cf89d
|
|||
16288c2bcb | |||
177b546fb9 | |||
311a37ae5c | |||
4380638982 | |||
32e0ddaa36 | |||
27c58b1174 | |||
d5f1325b79 | |||
9e27aabdb9 | |||
a1967141ae | |||
a172262dbd | |||
caee8c2a90 | |||
060c5f0c73 | |||
e2796b39b8 | |||
191c54cc9a | |||
87095a8c35 | |||
a28a678693 | |||
d3fd1e479c | |||
7f97f5330e | |||
30286710d1 | |||
8de018bf5c | |||
5a73910331 | |||
53236fab61 | |||
d5dfaaad96 | |||
c1e2974f1b | |||
40cdbfba36 | |||
be8777eb67 | |||
b75bd4f3b2 | |||
c127c62492 | |||
6a01631d22 | |||
1389233108 | |||
e2a4df8602 | |||
b4d8ab7606 | |||
478834fe2f | |||
ba441d5192
|
|||
4556452f69 | |||
6217103c5a | |||
eee6c2c2d2 | |||
db7d062662 | |||
c8a4067596
|
|||
437404dfca | |||
1b24596abf | |||
1759261332 | |||
3a9685511b | |||
7eac5d8bb3 | |||
68bc30d98b | |||
bc9b92cdd2 | |||
e49b30b2fb | |||
01a6cf34bc | |||
3ab85605c6 | |||
2bfc4886c8 | |||
e861bb4030 | |||
002044232f | |||
7facbbdeb2 | |||
55038a9745 | |||
665fb2e34c | |||
f0e168c684 | |||
e13ccd2e3e | |||
6e07adb59d | |||
7239df94dd | |||
ec7afca33d | |||
565ed66f70 | |||
12da87479c | |||
db3f0ed29a | |||
2b13b2e1ca | |||
914d1abf64 | |||
03564197a2 | |||
8e49322395
|
|||
af8c26511a
|
|||
5813338232
|
|||
29b8caab34
|
|||
3a78818e8d | |||
c53103352e | |||
442c1d2ca7 | |||
803506153f | |||
01205d6a1f
|
|||
ce40a002b7 | |||
7d5b649bd7 | |||
29937d3af2 | |||
b2cdd937b6 | |||
21c5a2ee53
|
|||
392b554eb9
|
|||
1c99aee43d
|
|||
d38c4d71a4
|
|||
bd34b874ca
|
|||
95986d170d | |||
fb58722216
|
|||
fa2f3acb35
|
|||
ecaa6fb68f
|
|||
3398324664
|
|||
6f7263dd84
|
|||
e87764a4c2
|
|||
5fb3c03e40 | |||
ac307de76c | |||
dee3ef4d85 |
30
.gitea/workflows/build-api.yml
Normal file
@ -0,0 +1,30 @@
|
||||
name: Build the docker image for the API
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["api/**"]
|
||||
|
||||
env:
|
||||
REGISTRY: git.ngn.tf
|
||||
IMAGE: ${{gitea.repository}}/api
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: "https://github.com/actions/checkout@v4"
|
||||
|
||||
- name: Login to container repo
|
||||
uses: "https://github.com/docker/login-action@v1"
|
||||
with:
|
||||
registry: ${{env.REGISTRY}}
|
||||
username: ${{gitea.actor}}
|
||||
password: ${{secrets.PACKAGES_TOKEN}}
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
cd api
|
||||
docker build --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest .
|
||||
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
35
.gitea/workflows/build-app.yml
Normal file
@ -0,0 +1,35 @@
|
||||
name: Build the docker image for the frontend application
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["app/**"]
|
||||
|
||||
env:
|
||||
REGISTRY: git.ngn.tf
|
||||
IMAGE: ${{gitea.repository}}/app
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: "https://github.com/actions/checkout@v4"
|
||||
|
||||
- name: Login to container repo
|
||||
uses: "https://github.com/docker/login-action@v1"
|
||||
with:
|
||||
registry: ${{env.REGISTRY}}
|
||||
username: ${{gitea.actor}}
|
||||
password: ${{secrets.PACKAGES_TOKEN}}
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
cd app
|
||||
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_DOC_URL=http://doc:7003 \
|
||||
--build-arg WEBSITE_API_URL=http://api:7002 \
|
||||
--build-arg WEBSITE_API_PATH=/api \
|
||||
--tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest .
|
||||
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
30
.gitea/workflows/build-doc.yml
Normal file
@ -0,0 +1,30 @@
|
||||
name: Build the docker image for the doc server
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["doc/**"]
|
||||
|
||||
env:
|
||||
REGISTRY: git.ngn.tf
|
||||
IMAGE: ${{gitea.repository}}/doc
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: "https://github.com/actions/checkout@v4"
|
||||
|
||||
- name: Login to container repo
|
||||
uses: "https://github.com/docker/login-action@v1"
|
||||
with:
|
||||
registry: ${{env.REGISTRY}}
|
||||
username: ${{gitea.actor}}
|
||||
password: ${{secrets.PACKAGES_TOKEN}}
|
||||
|
||||
- name: Build image
|
||||
run: |
|
||||
cd doc
|
||||
docker build --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest .
|
||||
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
21
.github/dependabot.yml
vendored
@ -1,21 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/app"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/api"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/api"
|
||||
schedule:
|
||||
interval: "weekly"
|
9
.gitignore
vendored
@ -1,2 +1,11 @@
|
||||
data.db
|
||||
*.yaml
|
||||
*.yml
|
||||
*.env
|
||||
|
||||
# don't ignore example deployment stuff
|
||||
!deploy/*
|
||||
!.github/*
|
||||
!.gitea/*
|
||||
!.github/*/*
|
||||
!.gitea/*/*
|
||||
|
13
Makefile
Normal file
@ -0,0 +1,13 @@
|
||||
SERVERS = app api doc
|
||||
|
||||
all: $(SERVERS)
|
||||
for server in $^ ; do \
|
||||
make -C $$server ; \
|
||||
done
|
||||
|
||||
format:
|
||||
for server in $(SERVERS) ; do \
|
||||
make -C $$server format ; \
|
||||
done
|
||||
|
||||
.PHONY: format
|
157
README.md
@ -1,94 +1,115 @@
|
||||
# website | my personal website
|
||||

|
||||

|
||||

|
||||
|
||||
This repo contains all the source code for my personal website, [ngn.tf](https://ngn.tf)
|
||||
All code is licensed under AGPL version 3 (see [LICENSE.txt](LICENSE.txt))
|
||||

|
||||

|
||||

|
||||
|
||||
This repo contains all the source code for my personal website,
|
||||
[ngn.tf](https://ngn.tf) All code is licensed under AGPL version 3 (see
|
||||
[LICENSE.txt](LICENSE.txt))
|
||||
|
||||
## Directory structure
|
||||
|
||||
### `app`
|
||||
|
||||
Contains frontend application, written with SvelteKit. It supports full SSR.
|
||||
Contains modified CSS from [github-markdown-css](https://github.com/sindresorhus/github-markdown-css)
|
||||
and fonts from [NerdFonts](https://www.nerdfonts.com/)
|
||||
Contains modified CSS from
|
||||
[github-markdown-css](https://github.com/sindresorhus/github-markdown-css) and
|
||||
fonts from [NerdFonts](https://www.nerdfonts.com/)
|
||||
|
||||
### `api`
|
||||
Contains the API server, written in Go. It uses the [Fiber](https://github.com/gofiber/fiber) web
|
||||
framework which offers an [Express](https://expressjs.com/) like experience. I choose Fiber since I've used worked with express a lot in the past. However previously the I was using
|
||||
|
||||
Contains the API server, written in Go. It uses the
|
||||
[Fiber](https://github.com/gofiber/fiber) web framework which offers an
|
||||
[Express](https://expressjs.com/) like experience. I choose Fiber since I've
|
||||
used worked with express a lot in the past. However previously the I was using
|
||||
[Gin](https://github.com/gin-gonic/gin) (see history section).
|
||||
|
||||
API stores all the data in a local sqlite(3) database. Go doesn't support sqlite3 out of the box so
|
||||
I'm using [mattn's sqlite3 driver](https://github.com/mattn/go-sqlite3).
|
||||
API stores all the data in a local SQLite(3) database. Go doesn't support
|
||||
SQLite3 out of the box so I'm using
|
||||
[mattn's sqlite3 driver](https://github.com/mattn/go-sqlite3).
|
||||
|
||||
### `doc`
|
||||
|
||||
Contains the documentation server, written in C. It uses the
|
||||
[ctorm](https://github.com/ngn13/ctorm) web framework, which is a framework that
|
||||
I myself wrote. Unlike the frontend application or the API server, it's not
|
||||
accessable by public, the frontend application gets the documentation content
|
||||
from this server and renders it using SSR. The reason I don't use the API for
|
||||
hosting the documentation content is that I want a separate server for hosting
|
||||
static content, API is only for hosting dynamic stuff.
|
||||
|
||||
### `admin`
|
||||
The frontend application does not contain an admin interface, I do the administration stuff (such as
|
||||
adding posts, adding services etc.) using the python script in this directory. This script can be
|
||||
installed on to the PATH by running the `install.sh` script. After installation it can be used
|
||||
by running `admin_script`.
|
||||
|
||||
The frontend application does not contain an admin interface, I do the
|
||||
administration stuff (such as adding news posts, adding services etc.) using the
|
||||
python script in this directory. This script can be installed on to the PATH by
|
||||
running the Makefile install script. After installation it can be used by
|
||||
running `admin_script`.
|
||||
|
||||
## Deployment
|
||||
Easiest way to deploy is to use docker. I have created a `compose.yml` file so the API and the
|
||||
frontend application can be deployed easily with just the `docker-compose up` command:
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ./app
|
||||
args:
|
||||
API_URL: https://api.ngn.tf
|
||||
ports:
|
||||
- "127.0.0.1:7002:3000"
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ./api
|
||||
environment:
|
||||
- API_PASSWORD="securepassword"
|
||||
- API_FRONTEND_URL="https://ngn.tf"
|
||||
ports:
|
||||
- "127.0.0.1:7001:7001"
|
||||
volumes:
|
||||
- ./api/data.db:/app/data.db
|
||||
```
|
||||
Easiest way to deploy is to use docker. There is `compose.yml` and a `run.sh`
|
||||
script in the [deploy](deploy/) directory that can be used to startup all the
|
||||
docker containers. Configuration options are passed during build time for the
|
||||
frontend application, and for others it's passed with environment variables.
|
||||
|
||||
## History
|
||||
|
||||
Some nostalgic history/changelog stuff (just for the major version numbers):
|
||||
|
||||
- **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was just a simple HTML/CSS page,
|
||||
I never published any of the source code and I wiped the local copy on my USB drive in early 2022
|
||||
- **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was
|
||||
just a simple HTML/CSS page, I never published any of the source code and I
|
||||
wiped the local copy on my USB drive in early 2022, I still remember what it
|
||||
looked like though, it looked like I made entire website in microsoft paint...
|
||||
while blindfoled, so yeah it was shit.
|
||||
|
||||
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code
|
||||
was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png)
|
||||
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my
|
||||
github.io page, and all the source code was (and still is) avaliable, it was
|
||||
just a simple static site, [here is a screenshot](assets/githubio.png).
|
||||
|
||||
- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework)
|
||||
my website with one of the fancy JS frameworks. I decided to go with Svelte. Not the kit version,
|
||||
at the time svelte did not support SSR. I do not remember writting an API for it so I guess I just
|
||||
updated it everytime I wanted to add content? It was pretty much like a static website and was hosted
|
||||
on `ngn13.fun` as at this point I had my own hosting. The source code for this website was in a
|
||||
deleted github repository of mine, I looked for a local copy on my old hard drive but I wasn't able
|
||||
to find it. I also do not remember how it looked like, sooo this version is pretty much lost :(
|
||||
- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite
|
||||
(and rework) my website with one of the fancy JS frameworks. I decided to go
|
||||
with Svelte. Not the kit version, at the time svelte did not support SSR. I do
|
||||
not remember writting an API for it so I guess I just updated it everytime I
|
||||
wanted to add content? It was pretty much like a static website and was hosted
|
||||
on `ngn13.fun` as at this point I had my own hosting. The source code for this
|
||||
website was in a deleted github repository of mine, I looked for a local copy
|
||||
on my old hard drive but I wasn't able to find it. I also do not remember how
|
||||
it looked like, sooo this version is pretty much lost :(
|
||||
|
||||
- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework
|
||||
my website one more time in NuxtJS. I had really "fun time" using vue stuff. As NuxtJS supported
|
||||
server-side code, this website had its own built in API. This website was also hosted on `ngn13.fun`
|
||||
- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided
|
||||
to rewrite and rework my website one more time in NuxtJS. I had really "fun"
|
||||
time using vue stuff. As NuxtJS supported server-side code, this website had
|
||||
its own built in API. This website was also hosted on `ngn13.fun`. This also
|
||||
the first version that lives on this git repository.
|
||||
|
||||
- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite and rework my website
|
||||
again, this time I was going with SvelteKit as I haven't had the greatest experience with NuxtJS.
|
||||
SvelteKit was really fun to work with and I got my new website done pretty quickly. (I don't wanna
|
||||
brag or something but I really imporeved the CSS/styling stuff ya know). I also wrote a new API
|
||||
with Go and Gin. I did not publish the source code for the API, its still on my local gitea
|
||||
server tho. This website was hosted on `ngn13.fun` as well
|
||||
- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite
|
||||
and rework my website again, this time I was going with SvelteKit as I haven't
|
||||
had the greatest experience with NuxtJS. SvelteKit was really fun to work with
|
||||
and I got my new website done pretty quickly. (I don't wanna brag or something
|
||||
but I really imporeved the CSS/styling stuff ya know). I also wrote a new API
|
||||
with Go and Gin. I did not publish the source code for the API, the code lived
|
||||
on my local git server until I deleted it when I was done with 6.0. This
|
||||
website was hosted on `ngn13.fun` as well.
|
||||
|
||||
- **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0,
|
||||
the big changes are in the API. I rewrote the API with Fiber. This version was the first version which is hosted on
|
||||
`ngn.tf` which is my new domain name btw
|
||||
- **v4.0 (2023 november - 2024 october)**: In this version the frontend was
|
||||
still similar to 3.0, the big changes are in the API. I rewrote the API with
|
||||
Fiber. This version was the first version hosted on `ngn.tf` which is my new
|
||||
domain name. Here is a [picture of the index](assets/4.0_index.png) and the
|
||||
[blog page](assets/4.0_blog.png).
|
||||
|
||||
- **v5.0 (2024 october - ...)**: The current major version of my website, has small UI and API tweaks when
|
||||
compared to 4.0
|
||||
- **v5.0 (2024 october - 2025 january)**: This version just had simple frontend
|
||||
UI changes compared to 4.0, at this point I was thinking about doing a massive
|
||||
rework (which I did with 6.0), however I was working on some other shit at the
|
||||
time, so I just did some small changes with the limited time I had for this
|
||||
project.
|
||||
|
||||
## Screenshots (from v4.0)
|
||||

|
||||

|
||||
- **v6.0 (2025 january - ...)**: The current major version of my website,
|
||||
frontend had a massive rework, API has been cleaned up and extended to do
|
||||
status checking for the services I host. The `doc` server has been added to
|
||||
the mix so I can host static documentation. The most important thing about
|
||||
this version is that it adds multi-language support, so literally everything
|
||||
on the website (including the API and documentation content) is localized for
|
||||
both English and Turkish, which was something I wanted to do for the longest
|
||||
time ever.
|
||||
|
349
admin/admin.py
@ -30,8 +30,6 @@ import requests as req
|
||||
from os import getenv
|
||||
from sys import argv
|
||||
|
||||
API_URL_ENV = "API_URL"
|
||||
|
||||
|
||||
# logger used by the script
|
||||
class Log:
|
||||
@ -138,11 +136,32 @@ class AdminAPI:
|
||||
|
||||
self.PUT("/v1/admin/service/add", service)
|
||||
|
||||
def del_service(self, service: str) -> None:
|
||||
if service == "":
|
||||
def del_service(self, name: str) -> None:
|
||||
if name == "":
|
||||
raise Exception("Service name cannot be empty")
|
||||
|
||||
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(service))
|
||||
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(name))
|
||||
|
||||
def add_project(self, project: Dict[str, str]):
|
||||
if "name" not in project or project["name"] == "":
|
||||
raise Exception('Project structure is missing required "name" field')
|
||||
|
||||
if "desc" not in project:
|
||||
raise Exception('Project structure is missing required "desc" field')
|
||||
|
||||
if not self._check_multilang_field(project["desc"]):
|
||||
raise Exception(
|
||||
'Project structure field "desc" needs at least '
|
||||
+ "one supported language entry"
|
||||
)
|
||||
|
||||
self.PUT("/v1/admin/project/add", project)
|
||||
|
||||
def del_project(self, name: str) -> None:
|
||||
if name == "":
|
||||
raise Exception("Project name cannot be empty")
|
||||
|
||||
self.DELETE("/v1/admin/project/del?name=%s" % quote_plus(name))
|
||||
|
||||
def check_services(self) -> None:
|
||||
self.GET("/v1/admin/service/check")
|
||||
@ -174,184 +193,216 @@ class AdminAPI:
|
||||
|
||||
self.PUT("/v1/admin/news/add", news)
|
||||
|
||||
def del_news(self, news: str) -> None:
|
||||
if news == "":
|
||||
def del_news(self, id: str) -> None:
|
||||
if id == "":
|
||||
raise Exception("News ID cannot be empty")
|
||||
|
||||
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(news))
|
||||
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(id))
|
||||
|
||||
def logs(self) -> List[Dict[str, Any]]:
|
||||
return self.GET("/v1/admin/logs")
|
||||
|
||||
|
||||
# local helper functions used by the script
|
||||
def __format_time(ts: int) -> str:
|
||||
class AdminScript:
|
||||
def __init__(self):
|
||||
self.log: Log = Log()
|
||||
self.api: AdminAPI = None
|
||||
self.commands = {
|
||||
"add_service": self.add_service,
|
||||
"del_service": self.del_service,
|
||||
"add_project": self.add_project,
|
||||
"del_project": self.del_project,
|
||||
"add_news": self.add_news,
|
||||
"del_news": self.del_news,
|
||||
"check_services": self.check_services,
|
||||
"logs": self.get_logs,
|
||||
}
|
||||
self.api_url_env = "API_URL"
|
||||
self.password_env = "API_PASSWORD"
|
||||
|
||||
def __format_time(self, ts: int) -> str:
|
||||
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
|
||||
|
||||
|
||||
def __load_json_file(file: str) -> Dict[str, Any]:
|
||||
def __load_json_file(self, file: str) -> Dict[str, Any]:
|
||||
with open(file, "r") as f:
|
||||
data = loads(f.read())
|
||||
return data
|
||||
|
||||
|
||||
def __dump_json_file(data: Dict[str, Any], file: str) -> None:
|
||||
def __dump_json_file(self, data: Dict[str, Any], file: str) -> None:
|
||||
with open(file, "w") as f:
|
||||
data = dumps(data, indent=2)
|
||||
f.write(data)
|
||||
|
||||
|
||||
# command handlers
|
||||
def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None:
|
||||
match cmd:
|
||||
case "add_service":
|
||||
data: Dict[str, str] = {}
|
||||
data["desc"] = {}
|
||||
|
||||
data["name"] = log.input("Serivce name")
|
||||
for lang in api.languages:
|
||||
data["desc"][lang] = log.input("Serivce desc (%s)" % lang)
|
||||
data["check_url"] = log.input("Serivce status check URL")
|
||||
data["clear"] = log.input("Serivce clearnet URL")
|
||||
data["onion"] = log.input("Serivce onion URL")
|
||||
data["i2p"] = log.input("Serivce I2P URL")
|
||||
|
||||
api.add_service(data)
|
||||
log.info("Service has been added")
|
||||
|
||||
case "del_service":
|
||||
api.del_service(log.input("Serivce name"))
|
||||
log.info("Service has been deleted")
|
||||
|
||||
case "check_services":
|
||||
api.check_services()
|
||||
log.info("Requested status check for all the services")
|
||||
|
||||
case "add_news":
|
||||
news: Dict[str, str] = {}
|
||||
news["title"] = {}
|
||||
news["content"] = {}
|
||||
|
||||
data["id"] = log.input("News ID")
|
||||
for lang in api.languages:
|
||||
data["title"][lang] = log.input("News title (%s)" % lang)
|
||||
data["author"] = log.input("News author")
|
||||
for lang in api.languages:
|
||||
data["content"][lang] = log.input("News content (%s)" % lang)
|
||||
|
||||
api.add_news(data)
|
||||
log.info("News has been added")
|
||||
|
||||
case "del_news":
|
||||
api.del_news(log.input("News ID"))
|
||||
log.info("News has been deleted")
|
||||
|
||||
case "logs":
|
||||
logs = api.logs()
|
||||
|
||||
if logs["result"] is None or len(logs["result"]) == 0:
|
||||
return log.info("No available logs")
|
||||
|
||||
for log in logs["result"]:
|
||||
log.info(
|
||||
"Time: %s | Action: %s"
|
||||
% (__format_time(log["time"]), log["action"])
|
||||
)
|
||||
|
||||
|
||||
def __handle_command_with_file(log: Log, api: AdminAPI, cmd: str, file: str) -> None:
|
||||
match cmd:
|
||||
case "add_service":
|
||||
data = __load_json_file(file)
|
||||
api.add_service(data)
|
||||
log.info("Service has been added")
|
||||
|
||||
case "del_service":
|
||||
data = __load_json_file(file)
|
||||
api.del_service(data["name"])
|
||||
log.info("Service has been deleted")
|
||||
|
||||
case "check_services":
|
||||
api.check_services()
|
||||
log.info("Requested status check for all the services")
|
||||
|
||||
case "add_news":
|
||||
data = __load_json_file(file)
|
||||
api.add_news(data)
|
||||
log.info("News has been added")
|
||||
|
||||
case "del_news":
|
||||
data = __load_json_file(file)
|
||||
api.del_news(data["id"])
|
||||
log.info("News has been deleted")
|
||||
|
||||
case "logs":
|
||||
logs = api.logs()
|
||||
|
||||
if logs["result"] is None or len(logs["result"]) == 0:
|
||||
return log.info("No available logs")
|
||||
|
||||
__dump_json_file(logs["result"], file)
|
||||
log.info("Logs has been saved")
|
||||
|
||||
|
||||
commands = [
|
||||
"add_service",
|
||||
"del_service",
|
||||
"check_services",
|
||||
"add_news",
|
||||
"del_news",
|
||||
"logs"
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
log = Log()
|
||||
|
||||
def run(self) -> bool:
|
||||
if len(argv) < 2 or len(argv) > 3:
|
||||
log.error("Usage: %s [command] <file>" % argv[0])
|
||||
log.info("Here is a list of available commands:")
|
||||
print("\tadd_service")
|
||||
print("\tdel_service")
|
||||
print("\tcheck_services")
|
||||
print("\tadd_news")
|
||||
print("\tdel_news")
|
||||
print("\tlogs")
|
||||
exit(1)
|
||||
self.log.error("Usage: %s [command] <file>" % argv[0])
|
||||
self.log.info("Here is a list of available commands:")
|
||||
|
||||
url = getenv(API_URL_ENV)
|
||||
for command in self.commands.keys():
|
||||
print("\t%s" % command)
|
||||
|
||||
return False
|
||||
|
||||
url = getenv(self.api_url_env)
|
||||
valid_cmd = False
|
||||
|
||||
for cmd in commands:
|
||||
if url is None:
|
||||
self.log.error(
|
||||
"Please specify the API URL using %s environment variable"
|
||||
% self.api_url_env
|
||||
)
|
||||
return False
|
||||
|
||||
for cmd in self.commands:
|
||||
if argv[1] == cmd:
|
||||
valid_cmd = True
|
||||
break
|
||||
|
||||
if not valid_cmd:
|
||||
log.error(
|
||||
self.log.error(
|
||||
"Invalid command, run the script with no commands to list the available commands"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
if url is None:
|
||||
log.error(
|
||||
"Please specify the API URL using %s environment variable" % API_URL_ENV
|
||||
)
|
||||
exit(1)
|
||||
return False
|
||||
|
||||
try:
|
||||
password = log.password("Please enter the admin password")
|
||||
api = AdminAPI(url, password)
|
||||
password = getenv(self.password_env)
|
||||
if password is None:
|
||||
password = self.log.password("Please enter the admin password")
|
||||
|
||||
self.api = AdminAPI(url, password)
|
||||
|
||||
if len(argv) == 2:
|
||||
__handle_command(log, api, argv[1])
|
||||
self.handle_command(argv[1])
|
||||
|
||||
elif len(argv) == 3:
|
||||
__handle_command_with_file(log, api, argv[1], argv[2])
|
||||
self.handle_command(argv[1], argv[2])
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
log.error("Command cancelled")
|
||||
exit(1)
|
||||
self.log.error("Command cancelled")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
log.error("Command failed: %s" % e)
|
||||
exit(1)
|
||||
self.log.error("Command failed: %s" % e)
|
||||
return False
|
||||
|
||||
# service commands
|
||||
def add_service(self, data: Dict[str, Any] = None) -> None:
|
||||
if data is None:
|
||||
data: Dict[str, str] = {}
|
||||
data["desc"] = {}
|
||||
|
||||
data["name"] = self.log.input("Serivce name")
|
||||
|
||||
for lang in self.api.languages:
|
||||
data["desc"][lang] = self.log.input("Serivce desc (%s)" % lang)
|
||||
|
||||
data["check_url"] = self.log.input("Serivce status check URL")
|
||||
data["clear"] = self.log.input("Serivce clearnet URL")
|
||||
data["onion"] = self.log.input("Serivce onion URL")
|
||||
data["i2p"] = self.log.input("Serivce I2P URL")
|
||||
|
||||
self.api.add_service(data)
|
||||
self.log.info("Service has been added")
|
||||
|
||||
def del_service(self, data: Dict[str, Any] = None) -> None:
|
||||
if data is None:
|
||||
data: Dict[str, str] = {}
|
||||
data["name"] = self.log.input("Service name")
|
||||
|
||||
self.api.del_service(data["name"])
|
||||
self.log.info("Service has been deleted")
|
||||
|
||||
# project commands
|
||||
def add_project(self, data: Dict[str, Any] = None) -> None:
|
||||
if data is None:
|
||||
data: Dict[str, str] = {}
|
||||
data["desc"] = {}
|
||||
|
||||
data["name"] = self.log.input("Project name")
|
||||
|
||||
for lang in self.api.languages:
|
||||
data["desc"][lang] = self.log.input("Project desc (%s)" % lang)
|
||||
|
||||
data["url"] = self.log.input("Project URL")
|
||||
data["license"] = self.log.input("Project license")
|
||||
|
||||
self.api.add_project(data)
|
||||
self.log.info("Project has been added")
|
||||
|
||||
def del_project(self, data: Dict[str, Any] = None) -> None:
|
||||
if data is None:
|
||||
data: Dict[str, str] = {}
|
||||
data["name"] = self.log.input("Project name")
|
||||
|
||||
self.api.del_project(data["name"])
|
||||
self.log.info("Project has been deleted")
|
||||
|
||||
# news command
|
||||
def add_news(self, data: Dict[str, Any] = None) -> None:
|
||||
if data is None:
|
||||
data: Dict[str, str] = {}
|
||||
data["title"] = {}
|
||||
data["content"] = {}
|
||||
|
||||
data["id"] = self.log.input("News ID")
|
||||
|
||||
for lang in self.api.languages:
|
||||
data["title"][lang] = self.log.input("News title (%s)" % lang)
|
||||
|
||||
data["author"] = self.log.input("News author")
|
||||
|
||||
for lang in self.api.languages:
|
||||
data["content"][lang] = self.log.input("News content (%s)" % lang)
|
||||
|
||||
self.api.add_news(data)
|
||||
self.log.info("News has been added")
|
||||
|
||||
def del_news(self, data: Dict[str, Any] = None) -> None:
|
||||
if data is None:
|
||||
data: Dict[str, str] = {}
|
||||
data["id"] = self.log.input("News ID")
|
||||
|
||||
self.api.del_project(data["id"])
|
||||
self.log.info("News has been deleted")
|
||||
|
||||
def check_services(self, data: Dict[str, Any] = None) -> None:
|
||||
self.api.check_services()
|
||||
self.log.info("Requested status check for all the services")
|
||||
|
||||
def get_logs(self, data: Dict[str, Any] = None) -> None:
|
||||
logs = self.api.logs()
|
||||
|
||||
if logs["result"] is None or len(logs["result"]) == 0:
|
||||
return self.log.info("No available logs")
|
||||
|
||||
for log in logs["result"]:
|
||||
self.log.info(
|
||||
"Time: %s | Action: %s"
|
||||
% (self.__format_time(log["time"]), log["action"])
|
||||
)
|
||||
|
||||
def handle_command(self, cmd: str, file: str = None) -> bool:
|
||||
for command in self.commands.keys():
|
||||
if command != cmd:
|
||||
continue
|
||||
|
||||
data = None
|
||||
|
||||
try:
|
||||
if file != "" and file is not None:
|
||||
data = self.__load_json_file(file)
|
||||
|
||||
self.commands[cmd](data)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.log.error("Command failed: %s" % e)
|
||||
return False
|
||||
|
||||
self.log.error("Invalid command: %s", cmd)
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
script = AdminScript()
|
||||
exit(0 if script.run() else 1)
|
||||
|
9
admin/tests/test_project.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "test",
|
||||
"desc": {
|
||||
"en": "A non-existent project used to test the API",
|
||||
"tr": "API'ı test etmek için kullanılan varolmayan bir proje"
|
||||
},
|
||||
"url": "https://github.com/ngn13/test",
|
||||
"license": "GPL-3.0"
|
||||
}
|
@ -1,19 +1,26 @@
|
||||
FROM golang:1.23.4
|
||||
FROM golang:1.24.4
|
||||
|
||||
WORKDIR /api
|
||||
|
||||
COPY *.go ./
|
||||
RUN useradd runner -r -u 1001 -d /api
|
||||
RUN chown -R runner:runner /api
|
||||
USER runner
|
||||
|
||||
COPY *.mod ./
|
||||
COPY *.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY *.go ./
|
||||
COPY Makefile ./
|
||||
COPY config ./config
|
||||
COPY database ./database
|
||||
COPY routes ./routes
|
||||
COPY sql ./sql
|
||||
COPY status ./status
|
||||
COPY util ./util
|
||||
COPY views ./views
|
||||
COPY routes ./routes
|
||||
COPY config ./config
|
||||
COPY status ./status
|
||||
COPY database ./database
|
||||
|
||||
EXPOSE 7001
|
||||
RUN make
|
||||
|
||||
ENTRYPOINT ["/api/api.elf"]
|
||||
|
@ -6,7 +6,7 @@ api.elf: $(GOSRCS)
|
||||
go build -o $@
|
||||
|
||||
run:
|
||||
API_DEBUG=true API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./api.elf
|
||||
WEBSITE_DEBUG=true WEBSITE_PASSWORD=test ./api.elf
|
||||
|
||||
format:
|
||||
gofmt -s -w .
|
||||
|
@ -3,111 +3,47 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/ngn13/ortam"
|
||||
)
|
||||
|
||||
type Type struct {
|
||||
Options []Option
|
||||
Count int
|
||||
Debug bool // should display debug messgaes?
|
||||
AppUrl *url.URL // frontend application URL for the website
|
||||
Password string // admin password
|
||||
Host string // host the server should listen on
|
||||
IPHeader string // header that should be checked for obtaining the client IP
|
||||
Interval string // service status check interval
|
||||
Timeout string // timeout for the service status check
|
||||
Limit string // if the service responds slower than this limit, it will be marked as "slow"
|
||||
}
|
||||
|
||||
func (c *Type) Find(name string, typ uint8) (*Option, error) {
|
||||
for i := 0; i < c.Count; i++ {
|
||||
if c.Options[i].Name != name {
|
||||
continue
|
||||
func Load() (*Type, error) {
|
||||
var conf = Type{
|
||||
Debug: false,
|
||||
Password: "",
|
||||
Host: "0.0.0.0:7002",
|
||||
IPHeader: "X-Real-IP",
|
||||
Interval: "1h",
|
||||
Timeout: "15s",
|
||||
Limit: "5s",
|
||||
}
|
||||
|
||||
if c.Options[i].Type != typ {
|
||||
return nil, fmt.Errorf("bad option type")
|
||||
if err := ortam.Load(&conf, "WEBSITE"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &c.Options[i], nil
|
||||
if conf.AppUrl == nil {
|
||||
conf.AppUrl, _ = url.Parse("http://localhost:7001/")
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("option not found")
|
||||
if conf.Password == "" {
|
||||
return nil, fmt.Errorf("password is not specified")
|
||||
}
|
||||
|
||||
func (c *Type) Load() (err error) {
|
||||
var (
|
||||
env_val string
|
||||
env_name string
|
||||
opt *Option
|
||||
exists bool
|
||||
)
|
||||
|
||||
// default options
|
||||
c.Options = []Option{
|
||||
{Name: "debug", Value: "false", Type: OPTION_TYPE_BOOL, Required: true}, // should display debug messgaes?
|
||||
{Name: "index", Value: "true", Type: OPTION_TYPE_BOOL, Required: false}, // should display the index page (view/index.md)?
|
||||
|
||||
{Name: "api_url", Value: "http://localhost:7001/", Type: OPTION_TYPE_URL, Required: true}, // API URL for the website
|
||||
{Name: "frontend_url", Value: "http://localhost:5173/", Type: OPTION_TYPE_URL, Required: true}, // frontend application URL for the website
|
||||
|
||||
{Name: "password", Value: "", Type: OPTION_TYPE_STR, Required: true}, // admin password
|
||||
{Name: "host", Value: "0.0.0.0:7001", 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: "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: "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)
|
||||
|
||||
for i := 0; i < c.Count; i++ {
|
||||
opt = &c.Options[i]
|
||||
|
||||
env_name = opt.Env()
|
||||
|
||||
if env_val, exists = os.LookupEnv(env_name); exists {
|
||||
opt.Value = env_val
|
||||
if conf.Host == "" {
|
||||
return nil, fmt.Errorf("host address is not specified")
|
||||
}
|
||||
|
||||
if opt.Value == "" && opt.Required {
|
||||
return fmt.Errorf("please specify a value for the config option \"%s\" (\"%s\")", opt.Name, env_name)
|
||||
}
|
||||
|
||||
if err = opt.Load(); err != nil {
|
||||
return fmt.Errorf("failed to load option \"%s\" (\"%s\"): %s", opt.Name, env_name, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Type) GetStr(name string) string {
|
||||
var (
|
||||
opt *Option
|
||||
err error
|
||||
)
|
||||
|
||||
if opt, err = c.Find(name, OPTION_TYPE_STR); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return opt.TypeValue.Str
|
||||
}
|
||||
|
||||
func (c *Type) GetBool(name string) bool {
|
||||
var (
|
||||
opt *Option
|
||||
err error
|
||||
)
|
||||
|
||||
if opt, err = c.Find(name, OPTION_TYPE_BOOL); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return opt.TypeValue.Bool
|
||||
}
|
||||
|
||||
func (c *Type) GetURL(name string) *url.URL {
|
||||
var (
|
||||
opt *Option
|
||||
err error
|
||||
)
|
||||
|
||||
if opt, err = c.Find(name, OPTION_TYPE_URL); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return opt.TypeValue.URL
|
||||
return &conf, nil
|
||||
}
|
||||
|
@ -1,49 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
OPTION_TYPE_STR = 0
|
||||
OPTION_TYPE_BOOL = 1
|
||||
OPTION_TYPE_URL = 2
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Name string
|
||||
Value string
|
||||
Required bool
|
||||
Type uint8
|
||||
TypeValue struct {
|
||||
URL *url.URL
|
||||
Str string
|
||||
Bool bool
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Option) Env() string {
|
||||
return strings.ToUpper(fmt.Sprintf("API_%s", o.Name))
|
||||
}
|
||||
|
||||
func (o *Option) Load() (err error) {
|
||||
err = nil
|
||||
|
||||
switch o.Type {
|
||||
case OPTION_TYPE_STR:
|
||||
o.TypeValue.Str = o.Value
|
||||
|
||||
case OPTION_TYPE_BOOL:
|
||||
o.TypeValue.Bool = "1" == o.Value || "true" == strings.ToLower(o.Value)
|
||||
|
||||
case OPTION_TYPE_URL:
|
||||
o.TypeValue.URL, err = url.Parse(o.Value)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("invalid option type")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
@ -24,8 +24,8 @@ func (db *Type) AdminLogNext(l *AdminLog) bool {
|
||||
var err error
|
||||
|
||||
if nil == db.rows {
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM admin_log"); err != nil {
|
||||
util.Fail("failed to query admin_log table: %s", err.Error())
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_ADMIN_LOG); err != nil {
|
||||
util.Fail("failed to query table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,7 @@ func (db *Type) AdminLogNext(l *AdminLog) bool {
|
||||
}
|
||||
|
||||
if err = l.Scan(db.rows); err != nil {
|
||||
util.Fail("failed to scan the admin_log table: %s", err.Error())
|
||||
util.Fail("failed to scan the table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ fail:
|
||||
|
||||
func (db *Type) AdminLogAdd(l *AdminLog) error {
|
||||
_, err := db.sql.Exec(
|
||||
`INSERT INTO admin_log(
|
||||
"INSERT INTO "+TABLE_ADMIN_LOG+`(
|
||||
action, time
|
||||
) values(?, ?)`,
|
||||
&l.Action, &l.Time,
|
@ -2,64 +2,62 @@ package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
const (
|
||||
SQL_PATH = "sql"
|
||||
|
||||
TABLE_ADMIN_LOG = "admin_log" // stores administrator logs
|
||||
TABLE_METRICS = "metrics" // stores API usage metrcis
|
||||
TABLE_NEWS = "news" // stores news posts
|
||||
TABLE_SERVICES = "services" // stores services
|
||||
TABLE_PROJECTS = "projects" // stores projects
|
||||
)
|
||||
|
||||
var tables []string = []string{
|
||||
TABLE_ADMIN_LOG, TABLE_METRICS, TABLE_NEWS,
|
||||
TABLE_SERVICES, TABLE_PROJECTS,
|
||||
}
|
||||
|
||||
type Type struct {
|
||||
sql *sql.DB
|
||||
rows *sql.Rows
|
||||
}
|
||||
|
||||
func (db *Type) Load() (err error) {
|
||||
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||
return fmt.Errorf("cannot access the database: %s", err.Error())
|
||||
func (db *Type) create_table(table string) error {
|
||||
var (
|
||||
err error
|
||||
query []byte
|
||||
)
|
||||
|
||||
query_path := path.Join(SQL_PATH, table+".sql")
|
||||
|
||||
if query, err = os.ReadFile(query_path); err != nil {
|
||||
return fmt.Errorf("failed to read %s for table %s: %", query_path, table, err.Error())
|
||||
}
|
||||
|
||||
// see database/service.go
|
||||
_, err = db.sql.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS services(
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
desc TEXT NOT NULL,
|
||||
check_time INTEGER NOT NULL,
|
||||
check_res INTEGER NOT NULL,
|
||||
check_url TEXT NOT NULL,
|
||||
clear TEXT,
|
||||
onion TEXT,
|
||||
i2p TEXT
|
||||
);
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the services table: %s", err.Error())
|
||||
}
|
||||
|
||||
// see database/news.go
|
||||
_, err = db.sql.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS news(
|
||||
id TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the news table: %s", err.Error())
|
||||
}
|
||||
|
||||
// see database/admin.go
|
||||
_, err = db.sql.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS admin_log(
|
||||
action TEXT NOT NULL,
|
||||
time INTEGER NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the admin_log table: %s", err.Error())
|
||||
if _, err = db.sql.Exec(string(query)); err != nil {
|
||||
return fmt.Errorf("failed to create the %s table: %s", table, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Type) Load() (err error) {
|
||||
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||
return fmt.Errorf("failed access the database: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
if err = db.create_table(table); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
57
api/database/metrics.go
Normal file
@ -0,0 +1,57 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/ngn13/website/api/util"
|
||||
)
|
||||
|
||||
func (db *Type) MetricsGet(key string) (uint64, error) {
|
||||
var (
|
||||
row *sql.Row
|
||||
count uint64
|
||||
err error
|
||||
)
|
||||
|
||||
if row = db.sql.QueryRow("SELECT value FROM "+TABLE_METRICS+" WHERE key = ?", key); row == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if err = row.Scan(&count); err != nil && err != sql.ErrNoRows {
|
||||
util.Fail("failed to scan the table: %s", err.Error())
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (db *Type) MetricsSet(key string, value uint64) error {
|
||||
var (
|
||||
err error
|
||||
res sql.Result
|
||||
)
|
||||
|
||||
if res, err = db.sql.Exec("UPDATE "+TABLE_METRICS+" SET value = ? WHERE key = ?", value, key); err != nil && err != sql.ErrNoRows {
|
||||
util.Fail("failed to query table: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if effected, err := res.RowsAffected(); err != nil {
|
||||
return err
|
||||
} else if effected < 1 {
|
||||
_, err = db.sql.Exec(
|
||||
"INSERT INTO "+TABLE_METRICS+`(
|
||||
key, value
|
||||
) values(?, ?)`,
|
||||
key, value,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -64,8 +64,8 @@ func (db *Type) NewsNext(n *News) bool {
|
||||
var err error
|
||||
|
||||
if nil == db.rows {
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM news"); err != nil {
|
||||
util.Fail("failed to query news table: %s", err.Error())
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_NEWS); err != nil {
|
||||
util.Fail("failed to query table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
}
|
||||
@ -75,7 +75,7 @@ func (db *Type) NewsNext(n *News) bool {
|
||||
}
|
||||
|
||||
if err = n.Scan(db.rows); err != nil {
|
||||
util.Fail("failed to scan the news table: %s", err.Error())
|
||||
util.Fail("failed to scan the table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@ fail:
|
||||
|
||||
func (db *Type) NewsRemove(id string) error {
|
||||
_, err := db.sql.Exec(
|
||||
"DELETE FROM news WHERE id = ?",
|
||||
"DELETE FROM "+TABLE_NEWS+" WHERE id = ?",
|
||||
id,
|
||||
)
|
||||
|
||||
@ -105,7 +105,7 @@ func (db *Type) NewsAdd(n *News) (err error) {
|
||||
}
|
||||
|
||||
_, err = db.sql.Exec(
|
||||
`INSERT OR REPLACE INTO news(
|
||||
"INSERT OR REPLACE INTO "+TABLE_NEWS+`(
|
||||
id, title, author, time, content
|
||||
) values(?, ?, ?, ?, ?)`,
|
||||
n.ID, n.title,
|
||||
|
92
api/database/project.go
Normal file
@ -0,0 +1,92 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/ngn13/website/api/util"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
Name string `json:"name"` // name of the project
|
||||
desc string `json:"-"` // description of the project (string)
|
||||
Desc Multilang `json:"desc"` // description of the project
|
||||
URL string `json:"url"` // URL of the project's homepage/source
|
||||
License string `json:"license"` // name of project's license
|
||||
}
|
||||
|
||||
func (p *Project) Load() error {
|
||||
return p.Desc.Load(p.desc)
|
||||
}
|
||||
|
||||
func (p *Project) Dump() (err error) {
|
||||
p.desc, err = p.Desc.Dump()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Project) Scan(rows *sql.Rows) (err error) {
|
||||
if err = rows.Scan(
|
||||
&p.Name, &p.desc,
|
||||
&p.URL, &p.License); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.Load()
|
||||
}
|
||||
|
||||
func (p *Project) IsValid() bool {
|
||||
return p.Name != "" && p.URL != "" && !p.Desc.Empty()
|
||||
}
|
||||
|
||||
func (db *Type) ProjectNext(p *Project) bool {
|
||||
var err error
|
||||
|
||||
if nil == db.rows {
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_PROJECTS); err != nil {
|
||||
util.Fail("failed to query table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
}
|
||||
|
||||
if !db.rows.Next() {
|
||||
goto fail
|
||||
}
|
||||
|
||||
if err = p.Scan(db.rows); err != nil {
|
||||
util.Fail("failed to scan the table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
fail:
|
||||
if db.rows != nil {
|
||||
db.rows.Close()
|
||||
}
|
||||
db.rows = nil
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *Type) ProjectRemove(name string) error {
|
||||
_, err := db.sql.Exec(
|
||||
"DELETE FROM "+TABLE_PROJECTS+" WHERE name = ?",
|
||||
name,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *Type) ProjectAdd(p *Project) (err error) {
|
||||
if err = p.Dump(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.sql.Exec(
|
||||
"INSERT OR REPLACE INTO "+TABLE_PROJECTS+`(
|
||||
name, desc, url, license
|
||||
) values(?, ?, ?, ?)`,
|
||||
p.Name, p.desc, p.URL, p.License,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
@ -58,8 +58,8 @@ func (db *Type) ServiceNext(s *Service) bool {
|
||||
var err error
|
||||
|
||||
if nil == db.rows {
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM services"); err != nil {
|
||||
util.Fail("failed to query services table: %s", err.Error())
|
||||
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_SERVICES); err != nil {
|
||||
util.Fail("failed to query table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
}
|
||||
@ -69,7 +69,7 @@ func (db *Type) ServiceNext(s *Service) bool {
|
||||
}
|
||||
|
||||
if err = s.Scan(db.rows, nil); err != nil {
|
||||
util.Fail("failed to scan the services table: %s", err.Error())
|
||||
util.Fail("failed to scan the table: %s", err.Error())
|
||||
goto fail
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ func (db *Type) ServiceFind(name string) (*Service, error) {
|
||||
err error
|
||||
)
|
||||
|
||||
if row = db.sql.QueryRow("SELECT * FROM services WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows {
|
||||
if row = db.sql.QueryRow("SELECT * FROM "+TABLE_SERVICES+" WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ func (db *Type) ServiceFind(name string) (*Service, error) {
|
||||
|
||||
func (db *Type) ServiceRemove(name string) error {
|
||||
_, err := db.sql.Exec(
|
||||
"DELETE FROM services WHERE name = ?",
|
||||
"DELETE FROM "+TABLE_SERVICES+" WHERE name = ?",
|
||||
name,
|
||||
)
|
||||
|
||||
@ -117,7 +117,7 @@ func (db *Type) ServiceUpdate(s *Service) (err error) {
|
||||
}
|
||||
|
||||
_, err = db.sql.Exec(
|
||||
`INSERT OR REPLACE INTO services(
|
||||
"INSERT OR REPLACE INTO "+TABLE_SERVICES+`(
|
||||
name, desc, check_time, check_res, check_url, clear, onion, i2p
|
||||
) values(?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
s.Name, s.desc,
|
||||
|
18
api/go.mod
@ -1,23 +1,23 @@
|
||||
module github.com/ngn13/website/api
|
||||
|
||||
go 1.21.3
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/gofiber/fiber/v2 v2.52.5
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/gofiber/fiber/v2 v2.52.8
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
github.com/ngn13/ortam v0.0.0-20250421004351-8dea81680817
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
)
|
||||
|
38
api/go.sum
@ -1,24 +1,30 @@
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
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=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
@ -27,5 +33,5 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
21
api/main.go
@ -36,18 +36,18 @@ func main() {
|
||||
app *fiber.App
|
||||
stat status.Type
|
||||
|
||||
conf config.Type
|
||||
conf *config.Type
|
||||
db database.Type
|
||||
|
||||
err error
|
||||
)
|
||||
|
||||
if err = conf.Load(); err != nil {
|
||||
if conf, err = config.Load(); err != nil {
|
||||
util.Fail("failed to load the configuration: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !conf.GetBool("debug") {
|
||||
if !conf.Debug {
|
||||
util.Debg = func(m string, v ...any) {}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if err = stat.Setup(&conf, &db); err != nil {
|
||||
if err = stat.Setup(conf, &db); err != nil {
|
||||
util.Fail("failed to setup the status checker: %s", err.Error())
|
||||
return
|
||||
}
|
||||
@ -75,7 +75,7 @@ func main() {
|
||||
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET") // POST can be sent from HTML forms, so I prefer PUT for API endpoints
|
||||
|
||||
c.Locals("status", &stat)
|
||||
c.Locals("config", &conf)
|
||||
c.Locals("config", conf)
|
||||
c.Locals("database", &db)
|
||||
|
||||
return c.Next()
|
||||
@ -89,14 +89,21 @@ func main() {
|
||||
|
||||
// v1 user routes
|
||||
v1.Get("/services", routes.GET_Services)
|
||||
v1.Get("/projects", routes.GET_Projects)
|
||||
v1.Get("/metrics", routes.GET_Metrics)
|
||||
v1.Get("/news/:lang", routes.GET_News)
|
||||
|
||||
// v1 admin routes
|
||||
v1.Use("/admin", routes.AuthMiddleware)
|
||||
v1.Get("/admin/logs", routes.GET_AdminLogs)
|
||||
|
||||
v1.Get("/admin/service/check", routes.GET_CheckService)
|
||||
v1.Put("/admin/service/add", routes.PUT_AddService)
|
||||
v1.Delete("/admin/service/del", routes.DEL_DelService)
|
||||
|
||||
v1.Put("/admin/project/add", routes.PUT_AddProject)
|
||||
v1.Delete("/admin/project/del", routes.DEL_DelProject)
|
||||
|
||||
v1.Put("/admin/news/add", routes.PUT_AddNews)
|
||||
v1.Delete("/admin/news/del", routes.DEL_DelNews)
|
||||
|
||||
@ -114,9 +121,9 @@ func main() {
|
||||
}
|
||||
|
||||
// start the app
|
||||
util.Info("starting web server on %s", conf.GetStr("host"))
|
||||
util.Info("starting web server on %s", conf.Host)
|
||||
|
||||
if err = app.Listen(conf.GetStr("host")); err != nil {
|
||||
if err = app.Listen(conf.Host); err != nil {
|
||||
util.Fail("failed to start the web server: %s", err.Error())
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ func admin_log(c *fiber.Ctx, m string) error {
|
||||
func AuthMiddleware(c *fiber.Ctx) error {
|
||||
conf := c.Locals("config").(*config.Type)
|
||||
|
||||
if c.Get("Authorization") != conf.GetStr("password") {
|
||||
if c.Get("Authorization") != conf.Password {
|
||||
return util.ErrAuth(c)
|
||||
}
|
||||
|
||||
@ -103,6 +103,56 @@ func GET_CheckService(c *fiber.Ctx) error {
|
||||
return util.JSON(c, 200, nil)
|
||||
}
|
||||
|
||||
func PUT_AddProject(c *fiber.Ctx) error {
|
||||
var (
|
||||
project database.Project
|
||||
err error
|
||||
)
|
||||
|
||||
db := c.Locals("database").(*database.Type)
|
||||
|
||||
if c.BodyParser(&project) != nil {
|
||||
return util.ErrBadJSON(c)
|
||||
}
|
||||
|
||||
if !project.IsValid() {
|
||||
return util.ErrBadReq(c)
|
||||
}
|
||||
|
||||
if err = admin_log(c, fmt.Sprintf("Added project \"%s\"", project.Name)); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if err = db.ProjectAdd(&project); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
return util.JSON(c, 200, nil)
|
||||
}
|
||||
|
||||
func DEL_DelProject(c *fiber.Ctx) error {
|
||||
var (
|
||||
name string
|
||||
err error
|
||||
)
|
||||
|
||||
db := c.Locals("database").(*database.Type)
|
||||
|
||||
if name = c.Query("name"); name == "" {
|
||||
util.ErrBadReq(c)
|
||||
}
|
||||
|
||||
if err = admin_log(c, fmt.Sprintf("Removed project \"%s\"", name)); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if err = db.ProjectRemove(name); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
return util.JSON(c, 200, nil)
|
||||
}
|
||||
|
||||
func DEL_DelNews(c *fiber.Ctx) error {
|
||||
var (
|
||||
id string
|
||||
|
@ -3,30 +3,11 @@ package routes
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/ngn13/website/api/config"
|
||||
"github.com/ngn13/website/api/util"
|
||||
)
|
||||
|
||||
func GET_Index(c *fiber.Ctx) error {
|
||||
var (
|
||||
md []byte
|
||||
err error
|
||||
)
|
||||
|
||||
conf := c.Locals("config").(*config.Type)
|
||||
|
||||
if !conf.GetBool("index") {
|
||||
return util.ErrNotFound(c)
|
||||
}
|
||||
|
||||
frontend := conf.GetURL("frontend_url")
|
||||
api := conf.GetURL("api_url")
|
||||
|
||||
if md, err = util.Render("views/index.md", fiber.Map{
|
||||
"frontend": frontend,
|
||||
"api": api,
|
||||
}); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
return util.Markdown(c, md)
|
||||
// redirect to the API documentation
|
||||
return c.Redirect(conf.AppUrl.JoinPath("/doc/api").String())
|
||||
}
|
||||
|
67
api/routes/metrics.go
Normal file
@ -0,0 +1,67 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/ngn13/website/api/database"
|
||||
"github.com/ngn13/website/api/util"
|
||||
)
|
||||
|
||||
const VISITOR_CACHE_MAX = 30 // store 30 visitor data at most
|
||||
var visitor_cache []string // in memory cache for the visitor addresses
|
||||
|
||||
func GET_Metrics(c *fiber.Ctx) error {
|
||||
var (
|
||||
err error
|
||||
result map[string]uint64 = map[string]uint64{
|
||||
"total": 0, // total number of visitors
|
||||
"since": 0, // metric collection start date (UNIX timestamp)
|
||||
}
|
||||
)
|
||||
|
||||
db := c.Locals("database").(*database.Type)
|
||||
new_addr := util.GetSHA1(util.IP(c))
|
||||
is_in_cache := false
|
||||
|
||||
for _, cache := range visitor_cache {
|
||||
if new_addr == cache {
|
||||
is_in_cache = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if result["total"], err = db.MetricsGet("visitor_count"); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if !is_in_cache {
|
||||
if len(visitor_cache) > VISITOR_CACHE_MAX {
|
||||
util.Debg("visitor cache is full, removing the oldest entry")
|
||||
visitor_cache = visitor_cache[1:]
|
||||
}
|
||||
|
||||
visitor_cache = append(visitor_cache, new_addr)
|
||||
result["total"]++
|
||||
|
||||
if err = db.MetricsSet("visitor_count", result["total"]); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
if result["since"], err = db.MetricsGet("start_date"); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
if result["since"] == 0 {
|
||||
result["since"] = uint64(time.Now().Truncate(24 * time.Hour).Unix())
|
||||
|
||||
if err = db.MetricsSet("start_date", result["since"]); err != nil {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
return util.JSON(c, 200, fiber.Map{
|
||||
"result": result,
|
||||
})
|
||||
}
|
@ -40,7 +40,6 @@ func GET_News(c *fiber.Ctx) error {
|
||||
|
||||
db := c.Locals("database").(*database.Type)
|
||||
conf := c.Locals("config").(*config.Type)
|
||||
frontend := conf.GetURL("frontend_url")
|
||||
lang := c.Params("lang")
|
||||
|
||||
if lang == "" || len(lang) != 2 {
|
||||
@ -63,7 +62,7 @@ func GET_News(c *fiber.Ctx) error {
|
||||
})
|
||||
|
||||
if feed, err = util.Render("views/news.xml", fiber.Map{
|
||||
"frontend": frontend,
|
||||
"app_url": conf.AppUrl,
|
||||
"updated": time.Now().Format(time.RFC3339),
|
||||
"entries": entries,
|
||||
"lang": lang,
|
||||
@ -71,6 +70,8 @@ func GET_News(c *fiber.Ctx) error {
|
||||
return util.ErrInternal(c, err)
|
||||
}
|
||||
|
||||
c.Set("Content-Disposition", "attachment; filename=\"news.atom\"")
|
||||
c.Set("Content-Type", "application/atom+xml; charset=utf-8")
|
||||
|
||||
return c.Send(feed)
|
||||
}
|
||||
|
24
api/routes/projects.go
Normal file
@ -0,0 +1,24 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/ngn13/website/api/database"
|
||||
"github.com/ngn13/website/api/util"
|
||||
)
|
||||
|
||||
func GET_Projects(c *fiber.Ctx) error {
|
||||
var (
|
||||
projects []database.Project
|
||||
project database.Project
|
||||
)
|
||||
|
||||
db := c.Locals("database").(*database.Type)
|
||||
|
||||
for db.ProjectNext(&project) {
|
||||
projects = append(projects, project)
|
||||
}
|
||||
|
||||
return util.JSON(c, 200, fiber.Map{
|
||||
"result": projects,
|
||||
})
|
||||
}
|
4
api/sql/admin_log.sql
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS admin_log(
|
||||
action TEXT NOT NULL,
|
||||
time INTEGER NOT NULL
|
||||
);
|
4
api/sql/metrics.sql
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS metrics(
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
value INTEGER NOT NULL
|
||||
);
|
7
api/sql/news.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS news(
|
||||
id TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
6
api/sql/projects.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS projects(
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
desc TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
license TEXT
|
||||
);
|
10
api/sql/services.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS services(
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
desc TEXT NOT NULL,
|
||||
check_time INTEGER NOT NULL,
|
||||
check_res INTEGER NOT NULL,
|
||||
check_url TEXT NOT NULL,
|
||||
clear TEXT,
|
||||
onion TEXT,
|
||||
i2p TEXT
|
||||
);
|
@ -67,28 +67,23 @@ func (s *Type) loop() {
|
||||
func (s *Type) Setup(conf *config.Type, db *database.Type) error {
|
||||
var (
|
||||
dur time.Duration
|
||||
iv, to, lm string
|
||||
err error
|
||||
)
|
||||
|
||||
iv = conf.GetStr("interval")
|
||||
to = conf.GetStr("timeout")
|
||||
lm = conf.GetStr("limit")
|
||||
|
||||
if iv == "" || to == "" || lm == "" {
|
||||
if conf.Interval == "" || conf.Timeout == "" || conf.Limit == "" {
|
||||
s.disabled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if dur, err = util.GetDuration(iv); err != nil {
|
||||
if dur, err = util.GetDuration(conf.Interval); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.timeout, err = util.GetDuration(iv); err != nil {
|
||||
if s.timeout, err = util.GetDuration(conf.Timeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.limit, err = util.GetDuration(iv); err != nil {
|
||||
if s.limit, err = util.GetDuration(conf.Limit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -6,29 +6,18 @@ import (
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/ngn13/website/api/config"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
func IP(c *fiber.Ctx) string {
|
||||
conf := c.Locals("config").(*config.Type)
|
||||
ip_header := conf.GetStr("ip_header")
|
||||
|
||||
if ip_header != "" && c.Get(ip_header) != "" {
|
||||
return strings.Clone(c.Get(ip_header))
|
||||
if conf.IPHeader != "" && c.Get(conf.IPHeader) != "" {
|
||||
return strings.Clone(c.Get(conf.IPHeader))
|
||||
}
|
||||
|
||||
return c.IP()
|
||||
}
|
||||
|
||||
func Markdown(c *fiber.Ctx, raw []byte) error {
|
||||
exts := blackfriday.FencedCode
|
||||
exts |= blackfriday.NoEmptyLineBeforeBlock
|
||||
exts |= blackfriday.HardLineBreak
|
||||
|
||||
c.Set("Content-Type", "text/html; charset=utf-8")
|
||||
return c.Send(blackfriday.Run(raw, blackfriday.WithExtensions(exts)))
|
||||
}
|
||||
|
||||
func JSON(c *fiber.Ctx, code int, data fiber.Map) error {
|
||||
if data == nil {
|
||||
data = fiber.Map{}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>{{.frontend.Host}} news</title>
|
||||
<title>{{.app_url.Host}} news</title>
|
||||
<updated>{{.updated}}</updated>
|
||||
<subtitle>News and updates about my projects and self-hosted services</subtitle>
|
||||
<link href="{{.frontend.JoinPath "/news"}}"></link>
|
||||
<link href="{{.app_url.JoinPath "/news"}}"></link>
|
||||
{{ range .entries }}
|
||||
<entry>
|
||||
<title>{{.Title}}</title>
|
||||
|
@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
build
|
||||
public
|
||||
build
|
||||
|
1
app/.gitignore
vendored
@ -8,3 +8,4 @@ node_modules
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
nerdfonts.*
|
||||
|
@ -1,14 +1,27 @@
|
||||
FROM node:23.5.0 as build
|
||||
# build the application with node
|
||||
FROM node:23.11.1 AS build
|
||||
|
||||
ARG WEBSITE_REPORT_URL
|
||||
ARG WEBSITE_SOURCE_URL
|
||||
ARG WEBSITE_DOC_URL
|
||||
ARG WEBSITE_API_URL
|
||||
ARG WEBSITE_API_PATH
|
||||
|
||||
ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL
|
||||
ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL
|
||||
ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL
|
||||
ENV WEBSITE_API_URL=$WEBSITE_API_URL
|
||||
ENV WEBSITE_API_PATH=$WEBSITE_API_PATH
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
|
||||
ARG API_URL
|
||||
ENV VITE_API_URL_DEV $API_URL
|
||||
RUN apt install -y make sed wget
|
||||
RUN npm install
|
||||
RUN make
|
||||
|
||||
RUN npm install && npm run build
|
||||
|
||||
FROM oven/bun:1.1.20 as main
|
||||
# run it with bun (a lot faster)
|
||||
FROM oven/bun:latest AS main
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@ -16,8 +29,13 @@ COPY --from=build /app/build ./build
|
||||
COPY --from=build /app/package.json ./package.json
|
||||
COPY --from=build /app/package-lock.json ./package-lock.json
|
||||
|
||||
EXPOSE 4173
|
||||
RUN useradd runner -r -u 1001 -d /app
|
||||
RUN chown -R runner:runner /app
|
||||
|
||||
USER runner
|
||||
RUN bun install
|
||||
|
||||
EXPOSE 7001
|
||||
|
||||
ENV PORT=7001
|
||||
CMD ["bun", "build/index.js"]
|
||||
|
25
app/Makefile
Normal file
@ -0,0 +1,25 @@
|
||||
NF_CSS = static/css/nerdfonts.css
|
||||
NF_WOFF = static/assets/nerdfonts.woff2
|
||||
|
||||
all: $(NF_CSS)
|
||||
npm run build
|
||||
|
||||
$(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:
|
||||
npm run format
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
rm $(NF_CSS)
|
||||
rm $(NF_WOFF)
|
||||
|
||||
.PHONY: format run clean
|
178
app/package-lock.json
generated
@ -1,20 +1,19 @@
|
||||
{
|
||||
"name": "website",
|
||||
"version": "5.0.0",
|
||||
"version": "6.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "website",
|
||||
"version": "5.0.0",
|
||||
"version": "6.3",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"dompurify": "^3.2.3",
|
||||
"marked": "^15.0.4",
|
||||
"marked": "^15.0.6",
|
||||
"svelte-i18n": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/adapter-node": "^5.2.11",
|
||||
"@sveltejs/kit": "^2.15.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.3",
|
||||
@ -832,23 +831,31 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-auto": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz",
|
||||
"integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"import-meta-resolve": "^4.1.0"
|
||||
"node_modules/@sveltejs/acorn-typescript": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
||||
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-auto": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.0.1.tgz",
|
||||
"integrity": "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-node": {
|
||||
"version": "5.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.11.tgz",
|
||||
"integrity": "sha512-lR7/dfUaKFf3aI408KRDy/BVDYoqUws7zNOJz2Hl4JoshlTnMgdha3brXBRFXB+cWtYvJjjPhvmq3xqpbioi4w==",
|
||||
"version": "5.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
|
||||
"integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
@ -860,24 +867,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.15.1.tgz",
|
||||
"integrity": "sha512-8t7D3hQHbUDMiaQ2RVnjJJ/+Ur4Fn/tkeySJCsHtX346Q9cp3LAnav8xXdfuqYNJwpUGX0x3BqF1uvbmXQw93A==",
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.0.tgz",
|
||||
"integrity": "sha512-DJm0UxVgzXq+1MUfiJK4Ridk7oIQsIets6JwHiEl97sI6nXScfXe+BeqNhzB7jQIVBb3BM51U4hNk8qQxRXBAA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.1.0",
|
||||
"esm-env": "^1.2.1",
|
||||
"import-meta-resolve": "^4.1.0",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
"mrmime": "^2.0.0",
|
||||
"sade": "^1.8.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"sirv": "^3.0.0",
|
||||
"tiny-glob": "^0.2.9"
|
||||
"vitefu": "^1.0.6"
|
||||
},
|
||||
"bin": {
|
||||
"svelte-kit": "svelte-kit.js"
|
||||
@ -886,16 +894,17 @@
|
||||
"node": ">=18.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"vite": "^5.0.3 || ^6.0.0"
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.3.tgz",
|
||||
"integrity": "sha512-J7nC5gT5qpmvyD2pmzPUntLUgoinyEaNy9sTpGGE6N7pblggO0A1NyneJJvR2ELlzK6ti28aF2SLXG1yJdnJeA==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz",
|
||||
"integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0",
|
||||
"debug": "^4.3.7",
|
||||
@ -912,11 +921,12 @@
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.0.tgz",
|
||||
"integrity": "sha512-hBxSYW/66989cq9dN248omD/ziskSdIV1NqfuueuAI1z6jGcg14k9Zd98pDIEnoA6wC9kWUGuQ6adzBbWwQyRg==",
|
||||
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
|
||||
"integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.7"
|
||||
},
|
||||
@ -936,15 +946,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/dompurify": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz",
|
||||
"integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==",
|
||||
"deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.",
|
||||
"dependencies": {
|
||||
"dompurify": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
@ -964,9 +965,10 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -974,14 +976,6 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-typescript": {
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
|
||||
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
|
||||
"peerDependencies": {
|
||||
"acorn": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
@ -1090,9 +1084,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
|
||||
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
@ -1188,9 +1183,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esm-env": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
|
||||
"integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng=="
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esniff": {
|
||||
"version": "2.0.1",
|
||||
@ -1208,9 +1204,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz",
|
||||
"integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==",
|
||||
"version": "1.4.9",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.9.tgz",
|
||||
"integrity": "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
}
|
||||
@ -1302,17 +1299,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/import-meta-resolve": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
|
||||
"integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/intl-messageformat": {
|
||||
"version": "10.7.11",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.11.tgz",
|
||||
@ -1399,9 +1385,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "15.0.4",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.4.tgz",
|
||||
"integrity": "sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw==",
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
@ -1529,9 +1516,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@ -1545,9 +1532,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-svelte": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.2.tgz",
|
||||
"integrity": "sha512-kRPjH8wSj2iu+dO+XaUv4vD8qr5mdDmlak3IT/7AOgGIMRG86z/EHOLauFcClKEnOUf4A4nOA7sre5KrJD4Raw==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz",
|
||||
"integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@ -1663,20 +1650,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.16.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.16.0.tgz",
|
||||
"integrity": "sha512-Ygqsiac6UogVED2ruKclU+pOeMThxWtp9LG+li7BXeDKC2paVIsRTMkNmcON4Zejerd1s5sZHWx6ZtU85xklVg==",
|
||||
"version": "5.34.7",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.34.7.tgz",
|
||||
"integrity": "sha512-5PEg+QQKce4t1qiOtVUhUS3AQRTtxJyGBTpxLcNWnr0Ve8q4r06bMo0Gv8uhtCPWlztZHoi3Ye7elLhu+PCTMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"acorn": "^8.12.1",
|
||||
"acorn-typescript": "^1.4.13",
|
||||
"aria-query": "^5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^1.3.2",
|
||||
"esrap": "^1.4.8",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
@ -2161,10 +2149,11 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
||||
"version": "5.4.19",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
|
||||
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@ -2220,12 +2209,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitefu": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.3.tgz",
|
||||
"integrity": "sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz",
|
||||
"integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"tests/deps/*",
|
||||
"tests/projects/*"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0"
|
||||
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vite": {
|
||||
|
@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "website",
|
||||
"version": "6.0",
|
||||
"version": "6.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "VITE_API_URL=http://127.0.0.1:7001 VITE_FRONTEND_URL=http://localhost:5173 vite dev",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host",
|
||||
"preview": "vite preview",
|
||||
"lint": "prettier --check .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/adapter-node": "^5.2.11",
|
||||
"@sveltejs/kit": "^2.15.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"svelte": "^5.16.0",
|
||||
@ -21,9 +21,8 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"dompurify": "^3.2.3",
|
||||
"marked": "^15.0.4",
|
||||
"marked": "^15.0.6",
|
||||
"svelte-i18n": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { locale } from "svelte-i18n";
|
||||
|
||||
export const handle = async ({ event, resolve }) => {
|
||||
const lang = event.request.headers.get("accept-language")?.split(",")[0];
|
||||
if (lang) locale.set(lang);
|
||||
return resolve(event);
|
||||
};
|
@ -1,21 +1,42 @@
|
||||
const version = "v1";
|
||||
const url = new URL(version + "/", import.meta.env.VITE_API_URL).href;
|
||||
import { browser } from "$app/environment";
|
||||
import { urljoin } from "$lib/util.js";
|
||||
|
||||
function join(path) {
|
||||
if (null === path || path === "") return url;
|
||||
const api_version = "v1";
|
||||
|
||||
if (path[0] === "/") path = path.slice(1);
|
||||
function api_urljoin(path = null, query = {}) {
|
||||
let api_url = "";
|
||||
|
||||
return new URL(path, url).href;
|
||||
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);
|
||||
}
|
||||
|
||||
async function services(fetch) {
|
||||
const res = await fetch(join("/services"));
|
||||
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();
|
||||
|
||||
if (!("result" in json)) return [];
|
||||
|
||||
return json.result;
|
||||
api_check_err(json);
|
||||
return json["result"];
|
||||
}
|
||||
|
||||
export { version, join, services };
|
||||
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 };
|
||||
|
@ -1,74 +0,0 @@
|
||||
<script>
|
||||
import { click } from "$lib/util.js";
|
||||
export let title;
|
||||
export let url;
|
||||
|
||||
let current = "";
|
||||
let i = 0;
|
||||
|
||||
while (title.length > i) {
|
||||
let c = title[i];
|
||||
setTimeout(
|
||||
() => {
|
||||
current += c;
|
||||
},
|
||||
100 * (i + 1)
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<a on:click={click} data-sveltekit-preload-data href={url}>
|
||||
<div class="title">
|
||||
{current}
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background: var(--dark-three);
|
||||
box-shadow: var(--box-shadow);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: 0.4s;
|
||||
text-decoration: none;
|
||||
border: solid 1px var(--border-color);
|
||||
}
|
||||
|
||||
a:hover > .title {
|
||||
text-shadow: var(--text-shadow);
|
||||
}
|
||||
|
||||
.title {
|
||||
border: solid 1px var(--dark-two);
|
||||
background: var(--dark-two);
|
||||
padding: 25px;
|
||||
border-radius: 7px 7px 0px 0px;
|
||||
font-size: 20px;
|
||||
font-family:
|
||||
Consolas,
|
||||
Monaco,
|
||||
Lucida Console,
|
||||
Liberation Mono,
|
||||
DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono,
|
||||
Courier New,
|
||||
monospace;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: var(--dark-three);
|
||||
padding: 30px;
|
||||
padding-top: 30px;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
font-size: 25px;
|
||||
}
|
||||
</style>
|
@ -1,25 +0,0 @@
|
||||
<script>
|
||||
export let class_name;
|
||||
</script>
|
||||
|
||||
<main class={class_name}>
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
|
||||
padding: 50px;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
main {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
28
app/src/lib/doc.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { urljoin } from "$lib/util.js";
|
||||
|
||||
function doc_urljoin(path = null, query = {}) {
|
||||
return urljoin(import.meta.env.WEBSITE_DOC_URL, path, query);
|
||||
}
|
||||
|
||||
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
@ -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>
|
@ -1,45 +1,49 @@
|
||||
<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 { color } from "$lib/util.js";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let visitor_count = 1001;
|
||||
let show_counter = false,
|
||||
data = {};
|
||||
|
||||
function should_congrat() {
|
||||
return visitor_count % 1000 == 0;
|
||||
}
|
||||
onMount(async () => {
|
||||
show_counter = true;
|
||||
data = await api_get_metrics(fetch);
|
||||
});
|
||||
</script>
|
||||
|
||||
<footer style="border-top: solid 2px var(--{color()});">
|
||||
<div class="info">
|
||||
<div class="links">
|
||||
<span>
|
||||
<Link href="/" bold={true}>{$_("footer.source")}</Link>
|
||||
<Link link={import.meta.env.WEBSITE_SOURCE_URL} bold={true}>{$_("footer.source")}</Link>
|
||||
</span>
|
||||
<span>/</span>
|
||||
<span>
|
||||
<Link href="/" bold={true}>{$_("footer.license")}</Link>
|
||||
<Link link="/doc/license" bold={true}>{$_("footer.license")}</Link>
|
||||
</span>
|
||||
<span>/</span>
|
||||
<span>
|
||||
<Link href="/" bold={true}>{$_("footer.privacy")}</Link>
|
||||
<Link link="/doc/privacy" bold={true}>{$_("footer.privacy")}</Link>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
{$_("footer.powered")}
|
||||
</span>
|
||||
</div>
|
||||
<div class="useless">
|
||||
<span>
|
||||
{$_("footer.number", { values: { count: visitor_count } })}
|
||||
{#if should_congrat()}
|
||||
<span style="color: var(--{color()})">({$_("footer.congrats")})</span>
|
||||
{#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>
|
||||
<span>
|
||||
{$_("footer.version", { values: { api_version: "v1", frontend_version: pkg.version } })}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="counter">{$_("footer.js")}</span>
|
||||
{/if}
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
@ -49,27 +53,29 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--black-1);
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 20px 50px 20px 50px;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
color: var(--white-2);
|
||||
font-size: var(--size-2);
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.useless {
|
||||
margin: 25px 50px 25px 0;
|
||||
span {
|
||||
color: var(--white-2);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.counter {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 25px 0 25px 50px;
|
||||
.links {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info .links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
|
@ -1,15 +1,24 @@
|
||||
<script>
|
||||
import { frontend_url, api_url } from "$lib/util.js";
|
||||
import { api_urljoin } from "$lib/api.js";
|
||||
export let desc, title;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>[ngn.tf] | {title}</title>
|
||||
|
||||
<meta content="[ngn.tf] | {title}" property="og:title" />
|
||||
<meta content={desc} property="og:description" />
|
||||
<meta content={frontend_url()} property="og:url" />
|
||||
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
||||
<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" />
|
||||
|
||||
<link rel="alternate" type="application/atom+xml" href={api_url("/news/en")} title="Atom Feed" />
|
||||
<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>
|
||||
|
@ -1,32 +1,55 @@
|
||||
<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 current = "";
|
||||
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(
|
||||
() => {
|
||||
current += title[i];
|
||||
},
|
||||
100 * (i + 1)
|
||||
);
|
||||
setTimeout(() => {
|
||||
title_cur += title[i];
|
||||
}, i * 70);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
show_animation = true;
|
||||
});
|
||||
|
||||
$: animate(title);
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<h1 style="color: var(--{color()})">{current}</h1>
|
||||
<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: linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96)),
|
||||
url("https://files.ngn.tf/banner.png");
|
||||
background: var(--background);
|
||||
background-size: 50%;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -35,15 +58,12 @@
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
img {
|
||||
padding: 50px 50px 0 50px;
|
||||
width: 220px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--size-7);
|
||||
header div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: end;
|
||||
padding: 50px 50px 30px 50px;
|
||||
font-size: var(--size-6);
|
||||
font-family:
|
||||
Consolas,
|
||||
Monaco,
|
||||
@ -53,29 +73,35 @@
|
||||
Bitstream Vera Sans Mono,
|
||||
Courier New,
|
||||
monospace;
|
||||
padding: 50px 50px 30px 50px;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: var(--text-shadow);
|
||||
justify-content: start;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
h1::after {
|
||||
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;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding: 80px;
|
||||
}
|
||||
|
||||
img {
|
||||
header img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
export default [
|
||||
{ code: "en", name: "English", icon: "🇬🇧", path: "../locales/en.json" },
|
||||
{ code: "tr", name: "Turkish", icon: "🇹🇷", path: "../locales/tr.json" },
|
||||
];
|
@ -5,10 +5,13 @@
|
||||
const default_color = "white-1";
|
||||
|
||||
export let active = false;
|
||||
export let highlight = true;
|
||||
export let link = "";
|
||||
export let icon = "";
|
||||
|
||||
let style = `text-decoration-color: var(--${color()});`;
|
||||
let style = "";
|
||||
|
||||
if (highlight) style = `text-decoration-color: var(--${color()});`;
|
||||
|
||||
if (active) style += `color: var(--${color()});`;
|
||||
else style += `color: var(--${default_color});`;
|
||||
@ -17,6 +20,18 @@
|
||||
{#if icon != ""}
|
||||
<Icon {icon} />
|
||||
{/if}
|
||||
<a {style} href={link}>
|
||||
{#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>
|
||||
|
66
app/src/lib/locale.js
Normal file
@ -0,0 +1,66 @@
|
||||
import { init, locale, register, waitLocale } from "svelte-i18n";
|
||||
import { browser } from "$app/environment";
|
||||
import { get, writable } from "svelte/store";
|
||||
|
||||
const locale_default = "en";
|
||||
let locale_index = writable(0);
|
||||
let locale_list = [];
|
||||
|
||||
function locale_setup() {
|
||||
// english
|
||||
register("en", () => import("../locales/en.json"));
|
||||
locale_list.push({ code: "en", name: "English", icon: "🇬🇧" });
|
||||
|
||||
// turkish
|
||||
register("tr", () => import("../locales/tr.json"));
|
||||
locale_list.push({ code: "tr", name: "Turkish", icon: "🇹🇷" });
|
||||
|
||||
init({
|
||||
fallbackLocale: locale_default,
|
||||
initialLocale: get(locale),
|
||||
});
|
||||
}
|
||||
|
||||
function locale_from_browser() {
|
||||
if (browser) return window.navigator.language.slice(0, 2).toLowerCase();
|
||||
else return locale_default;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
locale_list,
|
||||
locale_index,
|
||||
locale_default,
|
||||
locale_setup,
|
||||
locale_wait,
|
||||
locale_select,
|
||||
locale_from_browser,
|
||||
};
|
@ -18,13 +18,13 @@
|
||||
|
||||
<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;
|
||||
box-shadow: var(--def-shadow);
|
||||
}
|
||||
|
||||
div {
|
||||
|
@ -1,26 +1,33 @@
|
||||
<script>
|
||||
import { locale } from "svelte-i18n";
|
||||
import languages from "$lib/lang.js";
|
||||
let icon = "",
|
||||
indx = 0,
|
||||
len = languages.length;
|
||||
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() {
|
||||
if (indx >= languages.length) indx = 0;
|
||||
|
||||
icon = languages[indx].icon;
|
||||
locale.set(languages[indx++].code);
|
||||
locale_select(get_next($locale_index).code);
|
||||
}
|
||||
|
||||
for (indx = 0; indx < len; indx++) {
|
||||
if (languages[indx].code == $locale.slice(0, 2)) {
|
||||
icon = languages[indx++].icon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
onMount(() => {
|
||||
show = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<button on:click={next}>{icon}</button>
|
||||
{#if show}
|
||||
<button on:click={next}>
|
||||
{get_next($locale_index).icon}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
button {
|
||||
|
@ -3,38 +3,47 @@
|
||||
import Link from "$lib/link.svelte";
|
||||
|
||||
import { color, time_from_ts } from "$lib/util.js";
|
||||
import { locale } from "svelte-i18n";
|
||||
import { locale, _ } from "svelte-i18n";
|
||||
|
||||
export let service = {};
|
||||
let style = "";
|
||||
|
||||
if (service.check_res == 0) style = "opacity: 70%";
|
||||
</script>
|
||||
|
||||
<main {style}>
|
||||
<main>
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
<h1>{service.name}</h1>
|
||||
<p>{service.desc[$locale.slice(0, 2)]}</p>
|
||||
<p>{service.desc[$locale]}</p>
|
||||
</div>
|
||||
<div class="links">
|
||||
<Link link={service.clear}><Icon icon="nf-oct-link" /></Link>
|
||||
<Link highlight={false} link={service.clear}><Icon icon="nf-oct-link" /></Link>
|
||||
{#if service.onion != ""}
|
||||
<Link link={service.onion}><Icon icon="nf-linux-tor" /></Link>
|
||||
<Link highlight={false} link={service.onion}><Icon icon="nf-linux-tor" /></Link>
|
||||
{/if}
|
||||
{#if service.i2p != ""}
|
||||
<Link link={service.i2p}><span style="color: var(--{color()})">I2P</span></Link>
|
||||
<Link highlight={false} link={service.i2p}
|
||||
><span style="color: var(--{color()})">I2P</span></Link
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="check">
|
||||
<h1>Last checked at {time_from_ts(service.check_time)}</h1>
|
||||
<h1>
|
||||
{$_("services.last", {
|
||||
values: { time: time_from_ts(service.check_time) },
|
||||
})}
|
||||
</h1>
|
||||
{#if service.check_res == 0}
|
||||
<span style="background: var(--white-2)">Down</span>
|
||||
<span style="background: var(--white-2)">
|
||||
{$_("services.status.down")}
|
||||
</span>
|
||||
{:else if service.check_res == 1}
|
||||
<span style="background: var(--{color()})">Up</span>
|
||||
<span style="background: var(--{color()})">
|
||||
{$_("services.status.up")}
|
||||
</span>
|
||||
{:else if service.check_res == 2}
|
||||
<span style="background: var(--white-2)">Slow</span>
|
||||
<span style="background: var(--{color()}); filter: brightness(50%);">
|
||||
{$_("services.status.slow")}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
@ -58,16 +67,19 @@
|
||||
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 {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { join } from "$lib/api.js";
|
||||
import { locale_from_browser } from "$lib/locale.js";
|
||||
|
||||
const colors = [
|
||||
"yellow",
|
||||
@ -8,8 +8,8 @@ const colors = [
|
||||
"red",
|
||||
// "blue" (looks kinda ass)
|
||||
];
|
||||
|
||||
let colors_pos = -1;
|
||||
let api_url = join;
|
||||
|
||||
function color() {
|
||||
if (colors_pos < 0) colors_pos = Math.floor(Math.random() * colors.length);
|
||||
@ -19,17 +19,43 @@ function color() {
|
||||
}
|
||||
|
||||
function click() {
|
||||
let audio = new Audio("/click.wav");
|
||||
let audio = new Audio("/assets/click.wav");
|
||||
audio.play();
|
||||
}
|
||||
|
||||
function frontend_url(path) {
|
||||
if (null !== path && path !== "") return new URL(path, import.meta.env.VITE_FRONTEND_URL).href;
|
||||
else return new URL(import.meta.env.VITE_FRONTEND_URL).href;
|
||||
function urljoin(url, path = null) {
|
||||
if (undefined === url || null === url) return;
|
||||
|
||||
if (url[url.length - 1] != "/") url += "/";
|
||||
|
||||
if (null === path || "" === path) return url;
|
||||
if (path[0] === "/") return url + path.slice(1);
|
||||
return url + path;
|
||||
}
|
||||
|
||||
function time_from_ts(ts) {
|
||||
return new Date(ts * 1000).toLocaleTimeString();
|
||||
if (ts === 0 || ts === undefined) return;
|
||||
|
||||
let ts_date = new Date(ts * 1000);
|
||||
let ts_zone = ts_date.toString().match(/([A-Z]+[\+-][0-9]+)/)[1];
|
||||
|
||||
return (
|
||||
new Intl.DateTimeFormat(locale_from_browser(), {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}).format(ts_date) + ` (${ts_zone})`
|
||||
);
|
||||
}
|
||||
|
||||
export { api_url, frontend_url, click, color, time_from_ts };
|
||||
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 };
|
||||
|
@ -2,7 +2,6 @@
|
||||
"navbar": {
|
||||
"home": "home",
|
||||
"services": "services",
|
||||
"news": "news",
|
||||
"donate": "donate"
|
||||
},
|
||||
"home": {
|
||||
@ -10,7 +9,7 @@
|
||||
"welcome": {
|
||||
"title": "about",
|
||||
"desc": "Welcome to my website, I'm ngn",
|
||||
"whoami": "I'm a privacy, security and freedom addvocate high-schooler from Turkey",
|
||||
"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)"
|
||||
},
|
||||
@ -18,6 +17,7 @@
|
||||
"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"
|
||||
@ -25,25 +25,55 @@
|
||||
"links": {
|
||||
"title": "contact",
|
||||
"desc": "Here are some useful links if you want to get in contact with me",
|
||||
"prefer": "preferred"
|
||||
"prefer": "I highly prefer email, you can send encrypted emails using my PGP key"
|
||||
},
|
||||
"info": {
|
||||
"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 a 600 Mbit/s interface",
|
||||
"security": "All use SSL encrypted connection and they are all privacy-respecting",
|
||||
"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"
|
||||
"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",
|
||||
"powered": "Powered by Svelte, Go, SQLite and donations",
|
||||
"number": "You are the visitor number {count}",
|
||||
"congrat": "congrats!!",
|
||||
"version": "Using API version {api_version}, frontend version {frontend_version}"
|
||||
"number": "Visited {total} times since {since}",
|
||||
"wow": "wow!!",
|
||||
"js": "Enable javascript to display all the elements"
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
{
|
||||
"navbar": {
|
||||
"home": "anasayfa",
|
||||
"news": "haberler",
|
||||
"services": "servisler",
|
||||
"language": "dil"
|
||||
"donate": "bağış"
|
||||
},
|
||||
"home": {
|
||||
"title": "merhaba dünya!",
|
||||
"welcome": {
|
||||
"title": "Websiteme hoşgeldiniz, ben ngn",
|
||||
"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": "Zamanım çoğunlukla şunlar ile geçiyor...",
|
||||
"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",
|
||||
@ -21,26 +23,57 @@
|
||||
"wiki": "wikimi genişletmek"
|
||||
},
|
||||
"links": {
|
||||
"title": "Eğer benim ile iletişime geçmek istiyorsanız, işte bazı faydalı linkler",
|
||||
"prefer": "tercihim"
|
||||
"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"
|
||||
},
|
||||
"info": {
|
||||
"title": "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 600 Mbit/s ağ arayüzü üzerinden erişilebilir",
|
||||
"security": "Hepsi SSL şifreli bağlantı kullanıyor ve hepsi gizliğinize saygı gösteriyor",
|
||||
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
|
||||
"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 yok, boktan saçmalıklar yok",
|
||||
"link": "tüm servisleri incele"
|
||||
"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",
|
||||
"powered": "Svelte, Go, SQLite ve yemek param tarafından destekleniyor",
|
||||
"number": "{count}. ziyaretçisiniz",
|
||||
"congrat": "tebrikler!!",
|
||||
"version": "Kullan API versiyonu {api_version}, arayüz versiyonu {frontend_version}"
|
||||
"number": "{since} tarihinden beri {total} kez ziyaret edildi",
|
||||
"wow": "vay be!!",
|
||||
"js": "Tüm elementleri görüntelemek için javascript'i açın"
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,6 @@
|
||||
import { locale, waitLocale } from "svelte-i18n";
|
||||
import { init, register } from "svelte-i18n";
|
||||
import { browser } from "$app/environment";
|
||||
import languages from "$lib/lang.js";
|
||||
import { locale_setup, locale_wait } from "$lib/locale.js";
|
||||
|
||||
const defaultLocale = languages[0].code;
|
||||
|
||||
for (let i = 0; i < languages.length; i++)
|
||||
register(languages[i].code, () => import(/* @vite-ignore */ languages[i].path));
|
||||
|
||||
init({
|
||||
fallbackLocale: defaultLocale,
|
||||
initialLocale: browser ? window.navigator.language.slice(0, 2).toLowerCase() : defaultLocale,
|
||||
});
|
||||
|
||||
export const load = async () => {
|
||||
if (browser) locale.set(window.navigator.language);
|
||||
await waitLocale();
|
||||
};
|
||||
export async function load() {
|
||||
locale_setup();
|
||||
await locale_wait();
|
||||
}
|
||||
|
@ -1,18 +1,27 @@
|
||||
<script>
|
||||
import Navbar from "$lib/navbar.svelte";
|
||||
import Footer from "$lib/footer.svelte";
|
||||
|
||||
import { locale_select } from "$lib/locale.js";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
onMount(() => {
|
||||
locale_select();
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<Navbar />
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
{@render children()}
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@import "../../static/global.css";
|
||||
@import "/css/global.css";
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
|
15
app/src/routes/+page.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { api_get_projects } from "$lib/api.js";
|
||||
|
||||
export async function load({ fetch }) {
|
||||
try {
|
||||
let projects = await api_get_projects(fetch);
|
||||
return {
|
||||
projects: null === projects ? [] : projects,
|
||||
error: "",
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
error: err.toString(),
|
||||
};
|
||||
}
|
||||
}
|
@ -1,18 +1,31 @@
|
||||
<script>
|
||||
import { api_version } from "$lib/api.js";
|
||||
import Header from "$lib/header.svelte";
|
||||
import Content from "$lib/content.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 { browser } from "$app/environment";
|
||||
import { _, locale } from "svelte-i18n";
|
||||
import { color } from "$lib/util.js";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
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" />
|
||||
<Header title={$_("home.title")} picture="tired" />
|
||||
<Header picture="tired" title={$_("home.title")} />
|
||||
|
||||
<Content>
|
||||
{#if data.error.length !== 0}
|
||||
<Error error={data.error} />
|
||||
{:else}
|
||||
<main>
|
||||
<Card title={$_("home.welcome.title")}>
|
||||
<span> 👋 {$_("home.welcome.desc")}</span>
|
||||
<ul>
|
||||
@ -25,6 +38,7 @@
|
||||
<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>
|
||||
@ -34,68 +48,82 @@
|
||||
<span>{$_("home.links.desc")}:</span>
|
||||
<ul>
|
||||
<li>
|
||||
<Link icon="nf-fa-key" link="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D">
|
||||
<Link
|
||||
icon="nf-fa-key"
|
||||
link="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D"
|
||||
>
|
||||
PGP
|
||||
</Link>
|
||||
</li>
|
||||
<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>
|
||||
<li>
|
||||
<Link icon="nf-cod-github" link="https://github.com/ngn13">Github</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link icon="nf-md-email" link="mailto:ngn@ngn.tf">Email</Link>
|
||||
<span class="prefer">({$_("home.links.prefer")})</span>
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
<Card title={$_("home.info.title")}>
|
||||
<div class="services">
|
||||
<div class="info">
|
||||
<span>
|
||||
{$_("home.info.desc")}
|
||||
{$_("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.info.speed")}
|
||||
{$_("home.services.speed")}
|
||||
</li>
|
||||
<li>
|
||||
<i style="color: var(--{color()});" class="nf nf-fa-lock"></i>
|
||||
{$_("home.info.security")}
|
||||
{$_("home.services.security")}
|
||||
</li>
|
||||
<li>
|
||||
<i style="color: var(--{color()});" class="nf nf-fa-network_wired"></i>
|
||||
{$_("home.info.privacy")}
|
||||
{$_("home.services.privacy")}
|
||||
</li>
|
||||
<li>
|
||||
<i style="color: var(--{color()});" class="nf nf-md-eye_off"></i>
|
||||
{$_("home.info.bullshit")}
|
||||
{$_("home.services.bullshit")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Link link="/services">{$_("home.services.link")}</Link>
|
||||
</Card>
|
||||
</Content>
|
||||
<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>
|
||||
.prefer {
|
||||
color: var(--white-2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.services {
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
|
||||
padding: 50px;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.services .info {
|
||||
display: flex;
|
||||
@media only screen and (max-width: 900px) {
|
||||
main {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
12
app/src/routes/doc/[name]/+page.server.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { doc_get } from "$lib/doc";
|
||||
|
||||
export async function load({ fetch, params }) {
|
||||
try {
|
||||
return {
|
||||
doc: await doc_get(fetch, params.name),
|
||||
error: "",
|
||||
};
|
||||
} catch (err) {
|
||||
return { error: err.toString() };
|
||||
}
|
||||
}
|
50
app/src/routes/doc/[name]/+page.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script>
|
||||
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 { onMount } from "svelte";
|
||||
import { marked } from "marked";
|
||||
|
||||
let { data } = $props();
|
||||
marked.use({ breaks: true });
|
||||
|
||||
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" />
|
||||
<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 {
|
||||
padding: 50px;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
main .markdown-body :global(a) {
|
||||
color: var(--link-color);
|
||||
}
|
||||
</style>
|
@ -1,39 +1,39 @@
|
||||
<script>
|
||||
import Header from "$lib/header.svelte";
|
||||
import Head from "$lib/head.svelte";
|
||||
import Icon from "$lib/icon.svelte";
|
||||
|
||||
import { color } from "$lib/util.js";
|
||||
import { _ } from "svelte-i18n";
|
||||
</script>
|
||||
|
||||
<Head title="donate" desc="give me all of your life savings" />
|
||||
<Header title="donate money!" picture="money" />
|
||||
<Header picture="money" title={$_("donate.title")} />
|
||||
|
||||
<main>
|
||||
<span
|
||||
>I spend a lot of time working on different projects and maintaining different services.</span
|
||||
>
|
||||
<span
|
||||
>I also spend a lot of money, but unlike time, you don't usually get much of it for free.</span
|
||||
>
|
||||
<span
|
||||
>I mostly pay for hosting and electricity. Which when added up costs around 550₺ per month, that
|
||||
is Turkish Lira, equals to ~$15 at time of writing.</span
|
||||
>
|
||||
<span> </span>
|
||||
<span>
|
||||
{$_("donate.info")}
|
||||
{$_("donate.price")}
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<span
|
||||
>So even a small donation would be highly appreciated and it would help me keep everything up
|
||||
and running.</span
|
||||
>
|
||||
<span>
|
||||
{$_("donate.details")}
|
||||
</span>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="color: var(--{color()})">Platform</th>
|
||||
<th style="color: var(--{color()})">Address/Link</th>
|
||||
<th style="color: var(--{color()})">{$_("donate.table.platform")}</th>
|
||||
<th style="color: var(--{color()})">{$_("donate.table.address")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Monero (XMR)</td>
|
||||
<td>
|
||||
<Icon icon="nf-fa-monero" />
|
||||
Monero (XMR)
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
|
||||
@ -42,10 +42,9 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span
|
||||
>Also huge thanks to all of you who has donated so far, as I said, I highly appreciate it. Thank
|
||||
you!</span
|
||||
>
|
||||
<span>
|
||||
{$_("donate.thanks")}
|
||||
</span>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@ -77,6 +76,7 @@
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: var(--size-4);
|
||||
border: solid 1px var(--black-4);
|
||||
padding: 16px;
|
||||
}
|
||||
|
@ -1,7 +1,15 @@
|
||||
import { services } from "$lib/api.js";
|
||||
import { api_get_services } from "$lib/api.js";
|
||||
|
||||
export async function load({ fetch }) {
|
||||
try {
|
||||
let services = await api_get_services(fetch);
|
||||
return {
|
||||
list: await services(fetch),
|
||||
services: null === services ? [] : services,
|
||||
error: "",
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
error: err.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +1,70 @@
|
||||
<script>
|
||||
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_url } from "$lib/util.js";
|
||||
import { locale } from "svelte-i18n";
|
||||
import { api_urljoin } from "$lib/api.js";
|
||||
import { locale, _ } from "svelte-i18n";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let data;
|
||||
|
||||
let list = data.list,
|
||||
services = list;
|
||||
let value = "";
|
||||
let { data } = $props();
|
||||
let services = $state(data.services);
|
||||
let show_input = $state(false);
|
||||
|
||||
function change(input) {
|
||||
value = input.target.value.toLowerCase();
|
||||
let value = input.target.value.toLowerCase();
|
||||
services = [];
|
||||
|
||||
if (value === "") {
|
||||
services = list;
|
||||
services = data.services;
|
||||
return;
|
||||
}
|
||||
|
||||
list.forEach((s) => {
|
||||
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" />
|
||||
<Header title="service status" picture="cool" />
|
||||
<Header picture="cool" title={$_("services.title")} />
|
||||
|
||||
{#if data.error.length !== 0}
|
||||
<Error error={data.error} />
|
||||
{:else}
|
||||
<main>
|
||||
<div class="title">
|
||||
<input on:input={change} type="text" placeholder="Search for a service" />
|
||||
{#if show_input}
|
||||
<input oninput={change} type="text" placeholder={$_("services.search")} />
|
||||
{/if}
|
||||
<div>
|
||||
<Link icon="nf-fa-feed" link={api_url("/news/" + $locale.slice(0, 2))}>News and updates</Link>
|
||||
<Link icon="nf-fa-feed" link={api_urljoin("/news/" + $locale)}>{$_("services.feed")}</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="services">
|
||||
{#each services as service}
|
||||
{#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 {
|
||||
@ -58,12 +79,16 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main .none {
|
||||
color: var(--white-3);
|
||||
}
|
||||
|
||||
main .services {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-top: 20px;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
|
@ -1,85 +0,0 @@
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colorAnimation {
|
||||
100%,
|
||||
0% {
|
||||
color: rgb(255, 0, 0);
|
||||
}
|
||||
8% {
|
||||
color: rgb(255, 127, 0);
|
||||
}
|
||||
16% {
|
||||
color: rgb(255, 255, 0);
|
||||
}
|
||||
25% {
|
||||
color: rgb(127, 255, 0);
|
||||
}
|
||||
33% {
|
||||
color: rgb(0, 255, 0);
|
||||
}
|
||||
41% {
|
||||
color: rgb(0, 255, 127);
|
||||
}
|
||||
50% {
|
||||
color: rgb(0, 255, 255);
|
||||
}
|
||||
58% {
|
||||
color: rgb(0, 127, 255);
|
||||
}
|
||||
66% {
|
||||
color: rgb(0, 0, 255);
|
||||
}
|
||||
75% {
|
||||
color: rgb(127, 0, 255);
|
||||
}
|
||||
83% {
|
||||
color: rgb(255, 0, 255);
|
||||
}
|
||||
91% {
|
||||
color: rgb(255, 0, 127);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes borderAnimation {
|
||||
100%,
|
||||
0% {
|
||||
border-bottom-color: rgb(255, 0, 0);
|
||||
}
|
||||
8% {
|
||||
border-bottom-color: rgb(255, 127, 0);
|
||||
}
|
||||
16% {
|
||||
border-bottom-color: rgb(255, 255, 0);
|
||||
}
|
||||
25% {
|
||||
border-bottom-color: rgb(127, 255, 0);
|
||||
}
|
||||
33% {
|
||||
border-bottom-color: rgb(0, 255, 0);
|
||||
}
|
||||
41% {
|
||||
border-bottom-color: rgb(0, 255, 127);
|
||||
}
|
||||
50% {
|
||||
border-bottom-color: rgb(0, 255, 255);
|
||||
}
|
||||
58% {
|
||||
border-bottom-color: rgb(0, 127, 255);
|
||||
}
|
||||
66% {
|
||||
border-bottom-color: rgb(0, 0, 255);
|
||||
}
|
||||
75% {
|
||||
border-bottom-color: rgb(127, 0, 255);
|
||||
}
|
||||
83% {
|
||||
border-bottom-color: rgb(255, 0, 255);
|
||||
}
|
||||
91% {
|
||||
border-bottom-color: rgb(255, 0, 127);
|
||||
}
|
||||
}
|
BIN
app/static/assets/banner.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
11
app/static/css/animations.css
Normal file
@ -0,0 +1,11 @@
|
||||
@keyframes blink {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cursor {
|
||||
to {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
7
app/static/css/font.css
Normal 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");
|
||||
}
|
@ -1,4 +1,12 @@
|
||||
/*
|
||||
|
||||
* 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 {
|
||||
@ -25,10 +33,15 @@
|
||||
--size-4: 20px;
|
||||
--size-5: 24px;
|
||||
--size-6: 30px;
|
||||
--size-7: 50px;
|
||||
|
||||
--text-shadow: 0px 5px 20px rgba(90, 90, 90, 0.8);
|
||||
--box-shadow: rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px;
|
||||
--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;
|
||||
|
||||
--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;
|
||||
}
|
||||
|
||||
* {
|
||||
@ -36,6 +49,10 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--black-1);
|
||||
font-family: "Ubuntu", sans-serif;
|
@ -61,11 +61,6 @@
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
animation-name: colorAnimation;
|
||||
animation-iteration-count: infinite;
|
||||
animation-duration: 10s;
|
||||
background-color: transparent;
|
||||
color: #58a6ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@ -725,7 +720,7 @@
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
white-space: break-spaces;
|
||||
background: var(--dark-two);
|
||||
background: var(--black-3);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@ -770,8 +765,7 @@
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: var(--dark-two);
|
||||
border-radius: 6px;
|
||||
background-color: var(--black-3);
|
||||
}
|
||||
|
||||
.markdown-body pre code,
|
@ -1,10 +0,0 @@
|
||||
/* im using nerd fonts btw */
|
||||
@import "https://www.nerdfonts.com/assets/css/webfont.css";
|
||||
|
||||
@font-face {
|
||||
font-family: "Ubuntu";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url("/ubuntu.woff2") format("woff2");
|
||||
}
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
app/static/profile/reader.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
app/static/profile/sad.png
Normal file
After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
3
app/static/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
User-Agent: *
|
||||
Disallow: /doc/
|
||||
Disallow: /api/
|
@ -3,13 +3,51 @@ import { defineConfig } from "vite";
|
||||
import { fileURLToPath } from "url";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const file = fileURLToPath(new URL("package.json", import.meta.url));
|
||||
const json = readFileSync(file, "utf8");
|
||||
const pkg = JSON.parse(json);
|
||||
function env_from(prefix, object) {
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
let type = typeof value;
|
||||
let name = prefix + "_" + key.toUpperCase();
|
||||
|
||||
switch (type) {
|
||||
case "object":
|
||||
env_from(name, value);
|
||||
break;
|
||||
|
||||
case "string":
|
||||
if (process.env[name] === undefined) process.env[name] = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const default_env = {
|
||||
source_url: "https://git.ngn.tf/ngn/website",
|
||||
report_url: "https://git.ngn.tf/ngn/website/issues",
|
||||
doc_url: "http://localhost:7003",
|
||||
api: {
|
||||
url: "http://localhost:7002",
|
||||
path: "http://localhost:7002",
|
||||
},
|
||||
};
|
||||
|
||||
const package_file = fileURLToPath(new URL("package.json", import.meta.url));
|
||||
const package_json = readFileSync(package_file, "utf8");
|
||||
const package_data = JSON.parse(package_json);
|
||||
|
||||
env_from("WEBSITE", default_env);
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
envPrefix: "WEBSITE",
|
||||
preview: {
|
||||
port: 7001,
|
||||
strictPort: true,
|
||||
},
|
||||
server: {
|
||||
port: 7001,
|
||||
strictPort: true,
|
||||
},
|
||||
define: {
|
||||
pkg: pkg,
|
||||
pkg: package_data,
|
||||
},
|
||||
});
|
||||
|
62
deploy/compose.yml
Normal file
@ -0,0 +1,62 @@
|
||||
services:
|
||||
app:
|
||||
container_name: "website_app"
|
||||
image: website_app
|
||||
build:
|
||||
context: ./app
|
||||
args:
|
||||
WEBSITE_SOURCE_URL: "http://github.com/ngn13/website"
|
||||
WEBSITE_REPORT_URL: "http://github.com/ngn13/website/issues"
|
||||
WEBSITE_DOC_URL: "http://doc:7003"
|
||||
WEBSITE_API_URL: "http://api:7002"
|
||||
WEBSITE_API_PATH: "http://localhost:7002"
|
||||
security_opt:
|
||||
- "no-new-privileges:true"
|
||||
cap_drop:
|
||||
- ALL
|
||||
ports:
|
||||
- "127.0.0.1:7001:7001"
|
||||
depends_on:
|
||||
- api
|
||||
- doc
|
||||
read_only: true
|
||||
restart: unless-stopped
|
||||
|
||||
api:
|
||||
container_name: "website_api"
|
||||
image: website_api
|
||||
build:
|
||||
context: ./api
|
||||
security_opt:
|
||||
- "no-new-privileges:true"
|
||||
cap_drop:
|
||||
- ALL
|
||||
ports:
|
||||
- "127.0.0.1:7002:7002"
|
||||
volumes:
|
||||
- ./data.db:/api/data.db:rw
|
||||
environment:
|
||||
WEBSITE_DEBUG: "false"
|
||||
WEBSITE_APP_URL: "http://localhost:7001"
|
||||
WEBSITE_PASSWORD: "change_me"
|
||||
WEBSITE_HOST: "0.0.0.0:7002"
|
||||
WEBSITE_IP_HEADER: "X-Real-IP"
|
||||
WEBSITE_INTERVAL: "1h"
|
||||
WEBSITE_TIMEOUT: "15s"
|
||||
WEBSITE_LIMIT: "5s"
|
||||
restart: unless-stopped
|
||||
|
||||
doc:
|
||||
container_name: "website_doc"
|
||||
image: website_doc
|
||||
build:
|
||||
context: ./doc
|
||||
security_opt:
|
||||
- "no-new-privileges:true"
|
||||
cap_drop:
|
||||
- ALL
|
||||
environment:
|
||||
WEBSITE_HOST: "0.0.0.0:7003"
|
||||
WEBSITE_DOCS_DIR: "./docs"
|
||||
read_only: true
|
||||
restart: unless-stopped
|
9
deploy/run.sh
Normal file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f data.db ]; then
|
||||
touch data.db
|
||||
sudo chmod 1001:1001 data.db
|
||||
fi
|
||||
|
||||
docker-compose build
|
||||
docker-compose up -d
|
225
doc/.clang-format
Normal file
@ -0,0 +1,225 @@
|
||||
---
|
||||
Language: Cpp
|
||||
# BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -2
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignArrayOfStructures: Left
|
||||
AlignConsecutiveAssignments:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveDeclarations:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: true
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments:
|
||||
Kind: Always
|
||||
OverEmptyLines: 0
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortEnumsOnASingleLine: true
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: MultiLine
|
||||
AttributeMacros:
|
||||
- __capability
|
||||
BinPackArguments: false
|
||||
BinPackParameters: true
|
||||
BitFieldColonSpacing: Both
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterExternBlock: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
BeforeLambdaBody: false
|
||||
BeforeWhile: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakAfterAttributes: Never
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakArrays: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeConceptDeclarations: Always
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeInlineASMColon: OnlyMultiline
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 80
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
EmptyLineAfterAccessModifier: Never
|
||||
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IfMacros:
|
||||
- KJ_IF_MAYBE
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentAccessModifiers: false
|
||||
IndentCaseBlocks: false
|
||||
IndentCaseLabels: false
|
||||
IndentExternBlock: AfterExternBlock
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentRequiresClause: true
|
||||
IndentWidth: 2
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: false
|
||||
InsertNewlineAtEOF: false
|
||||
InsertTrailingCommas: None
|
||||
IntegerLiteralSeparator:
|
||||
Binary: 0
|
||||
BinaryMinDigits: 0
|
||||
Decimal: 0
|
||||
DecimalMinDigits: 0
|
||||
Hex: 0
|
||||
HexMinDigits: 0
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
LambdaBodyIndentation: Signature
|
||||
LineEnding: DeriveLF
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCBreakBeforeNestedBlockParam: true
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PackConstructorInitializers: BinPack
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakOpenParenthesis: 0
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyIndentedWhitespace: 0
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Right
|
||||
PPIndentWidth: -1
|
||||
QualifierAlignment: Leave
|
||||
ReferenceAlignment: Pointer
|
||||
ReflowComments: true
|
||||
RemoveBracesLLVM: false
|
||||
RemoveSemicolon: false
|
||||
RequiresClausePosition: OwnLine
|
||||
RequiresExpressionIndentation: OuterScope
|
||||
SeparateDefinitionBlocks: Leave
|
||||
ShortNamespaceLines: 1
|
||||
SortIncludes: false
|
||||
SortJavaStaticImport: Before
|
||||
SortUsingDeclarations: LexicographicNumeric
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceAroundPointerQualifiers: Default
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCaseColon: false
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeParensOptions:
|
||||
AfterControlStatements: true
|
||||
AfterForeachMacros: true
|
||||
AfterFunctionDefinitionName: false
|
||||
AfterFunctionDeclarationName: false
|
||||
AfterIfMacros: true
|
||||
AfterOverloadedOperator: false
|
||||
AfterRequiresInClause: false
|
||||
AfterRequiresInExpression: false
|
||||
BeforeNonEmptyParentheses: false
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceBeforeSquareBrackets: false
|
||||
SpaceInEmptyBlock: false
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: -1
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Latest
|
||||
StatementAttributeLikeMacros:
|
||||
- Q_EMIT
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 8
|
||||
UseTab: Never
|
||||
WhitespaceSensitiveMacros:
|
||||
- BOOST_PP_STRINGIZE
|
||||
- CF_SWIFT_NAME
|
||||
- NS_SWIFT_NAME
|
||||
- PP_STRINGIZE
|
||||
- STRINGIZE
|
||||
...
|
||||
|
35
doc/.clang-tidy
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
# "gnu-zero-variadic-macro-arguments" ignored because we are using GNU99
|
||||
# standart
|
||||
|
||||
# "clang-diagnostic-language-extension-token" is ignored because we need the
|
||||
# asm() extension token
|
||||
|
||||
# "DeprecatedOrUnsafeBufferHandling" ignored because C11 "_s" functions are not
|
||||
# secure either
|
||||
Checks: >-
|
||||
clang-diagnostic-*,
|
||||
-clang-diagnostic-gnu-zero-variadic-macro-arguments,
|
||||
-clang-diagnostic-language-extension-token,
|
||||
clang-analyzer-*,
|
||||
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
|
||||
portability-*,
|
||||
performance-*,
|
||||
WarningsAsErrors: '*'
|
||||
HeaderFileExtensions:
|
||||
- ''
|
||||
- h
|
||||
- hh
|
||||
- hpp
|
||||
- hxx
|
||||
ImplementationFileExtensions:
|
||||
- c
|
||||
- cc
|
||||
- cpp
|
||||
- cxx
|
||||
HeaderFilterRegex: '.*'
|
||||
ExcludeHeaderFilterRegex: ''
|
||||
FormatStyle: file
|
||||
SystemHeaders: false
|
||||
...
|
||||
|
4
doc/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
compile_commands.json
|
||||
.cache
|
||||
*.elf
|
||||
dist
|
17
doc/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM ghcr.io/ngn13/ctorm:1.8.1
|
||||
|
||||
WORKDIR /doc
|
||||
|
||||
COPY Makefile ./
|
||||
COPY pages ./pages
|
||||
COPY inc ./inc
|
||||
COPY src ./src
|
||||
|
||||
RUN useradd runner -r -u 1001 -d /doc
|
||||
RUN chown -R runner:runner /doc
|
||||
|
||||
USER runner
|
||||
RUN make
|
||||
|
||||
EXPOSE 7003
|
||||
ENTRYPOINT ["/doc/doc.elf"]
|
38
doc/Makefile
Normal file
@ -0,0 +1,38 @@
|
||||
# dirs
|
||||
DIRS = $(shell find src/* -type d)
|
||||
DISTDIR = dist
|
||||
OUTDIRS = $(patsubst src/%,$(DISTDIR)/%,$(DIRS))
|
||||
|
||||
# sources
|
||||
HSRCS = $(wildcard inc/*.h)
|
||||
CSRCS = $(shell find -type f -name '*.c')
|
||||
OBJS = $(patsubst ./src/%.c,./$(DISTDIR)/%.o,$(CSRCS))
|
||||
|
||||
# compiler flags
|
||||
CFLAGS = -O3 -fstack-protector-strong -fcf-protection=full -fstack-clash-protection
|
||||
LIBS = -lctorm -lcjson
|
||||
INCLUDE = -I./inc
|
||||
|
||||
all: doc.elf
|
||||
|
||||
doc.elf: $(OBJS)
|
||||
echo $(OBJS) $(OUTDIRS)
|
||||
gcc $(CFLAGS) -o $@ $^ $(LIBS)
|
||||
|
||||
$(DISTDIR)/%.o: src/%.c
|
||||
@mkdir -pv $(OUTDIRS)
|
||||
gcc $(CFLAGS) $(INCLUDE) -c -o $@ $^ $(LIBS)
|
||||
|
||||
format:
|
||||
clang-format -i -style=file $(CSRCS) $(HSRCS)
|
||||
|
||||
lint:
|
||||
clang-tidy --warnings-as-errors --config= $(CSRCS) $(HSRCS)
|
||||
|
||||
clean:
|
||||
rm -rf $(DISTDIR)
|
||||
|
||||
run:
|
||||
./doc.elf
|
||||
|
||||
.PHONY: format lint clean run
|
19
doc/inc/config.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
#define OPT_NAME_MAX 20
|
||||
char name[20]; // option name
|
||||
char *value; // option value
|
||||
bool required; // is the option required (does it need to have a value)
|
||||
} option_t;
|
||||
|
||||
typedef struct config {
|
||||
option_t *options;
|
||||
int32_t count;
|
||||
} config_t;
|
||||
|
||||
bool config_load(config_t *conf);
|
||||
char *config_get(config_t *conf, const char *name);
|
18
doc/inc/docs.h
Normal file
@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <linux/limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <dirent.h>
|
||||
|
||||
#include "file.h"
|
||||
|
||||
typedef struct {
|
||||
DIR *dir;
|
||||
file_t *file;
|
||||
char name[NAME_MAX + 1];
|
||||
char *lang;
|
||||
} docs_t;
|
||||
|
||||
bool docs_init(docs_t *docs, char *dir);
|
||||
char *docs_next(docs_t *docs, char *name, bool content);
|
||||
void docs_reset(docs_t *docs);
|
||||
void docs_free(docs_t *docs);
|
10
doc/inc/file.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
char *content;
|
||||
int64_t size;
|
||||
} file_t;
|
||||
|
||||
file_t *file_load(int dirfd, char *path);
|
||||
void file_free(file_t *file);
|
7
doc/inc/routes.h
Normal file
@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include <ctorm/ctorm.h>
|
||||
|
||||
void route_cors(ctorm_req_t *req, ctorm_res_t *res);
|
||||
void route_list(ctorm_req_t *req, ctorm_res_t *res);
|
||||
void route_get(ctorm_req_t *req, ctorm_res_t *res);
|
||||
void route_notfound(ctorm_req_t *req, ctorm_res_t *res);
|
11
doc/inc/util.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <ctorm/ctorm.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#define util_toupper(str) \
|
||||
for (char *c = str; *c != 0; c++) \
|
||||
*c = toupper(*c)
|
||||
uint64_t util_endswith(char *str, char *suf);
|
||||
void util_send(ctorm_res_t *res, uint16_t code, cJSON *json);
|
4
doc/pages/api.en.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "API",
|
||||
"desc": "Website's API documentation"
|
||||
}
|
@ -1,40 +1,27 @@
|
||||
<!-- This is the markdown file that will be served by the index route -->
|
||||
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.
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
# [{{.api.Host}}]({{.api.String}})
|
||||
This is the API for my personal website, [{{.frontend.Host}}]({{.frontend.String}}).
|
||||
|
||||
It stores information about the self-hosted services I provide and it also allows me
|
||||
to publish news and updates about these services using an Atom feed. It's written in
|
||||
Go and uses SQLite for storage. Licensed under GNU GPL version 3.
|
||||
|
||||
**Source code and the license is available at**: [https://github.com/ngn13/website](https://github.com/ngn13/website)
|
||||
**You can report issues to**: [https://github.com/ngn13/website/issues](https://github.com/ngn13/website/issues)
|
||||
|
||||
The rest of this document contains documentation for 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
|
||||
Each version 1 endpoint, can be accessed using the /v1 route.
|
||||
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
|
||||
`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
|
||||
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
|
||||
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`)
|
||||
@ -49,14 +36,14 @@ Here is an example multilang structure:
|
||||
"tr": "Merhaba, dünya!"
|
||||
}
|
||||
```
|
||||
If a "result" field is using a multilang structure, it will be specified as "Multilang"
|
||||
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 and 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:
|
||||
@ -91,12 +78,24 @@ not supported for this service/status checking is disabled (integer, UNIX timest
|
||||
- `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".
|
||||
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.
|
||||
|
||||
### GET /v1/metrics
|
||||
Returns metrics about the API usage. The metric data has the following format:
|
||||
```
|
||||
{
|
||||
"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:
|
||||
```
|
||||
@ -109,18 +108,16 @@ Where:
|
||||
- `action`: Action that the administrator performed (string)
|
||||
- `time`: Time when the administrator action was performed (integer, UNIX timestamp)
|
||||
|
||||
Client can get the logs for only a single address, by setting the URL query "addr".
|
||||
|
||||
### 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
|
||||
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".
|
||||
setting the URL query `name`.
|
||||
|
||||
Returns no data on success.
|
||||
|
||||
@ -156,6 +153,6 @@ 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".
|
||||
by setting the URL query `id`.
|
||||
|
||||
Returns no data on success.
|
4
doc/pages/api.tr.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "API",
|
||||
"desc": "Websitesinin API dökümantasyonu"
|
||||
}
|
160
doc/pages/api.tr.md
Normal file
@ -0,0 +1,160 @@
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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:
|
||||
- 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### GET /v1/services
|
||||
Erişilebilir servislerin bir listesini döndürür. Her servis şu JSON formatını
|
||||
takip eder:
|
||||
```
|
||||
{
|
||||
"name": "Test Service",
|
||||
"desc": {
|
||||
"en": "Service used for testing the API",
|
||||
"tr": "API'ı test etmek için kullanılan servis"
|
||||
},
|
||||
"check_time": 1735861944,
|
||||
"check_res": 1,
|
||||
"check_url": "http://localhost:7001",
|
||||
"clear": "http://localhost:7001",
|
||||
"onion": "",
|
||||
"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_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)
|
||||
- `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.
|
||||
|
||||
### GET /v1/news/:language
|
||||
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
|
||||
}
|
||||
```
|
||||
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:
|
||||
```
|
||||
{
|
||||
"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.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
```
|
||||
{
|
||||
"id": "test_news",
|
||||
"title": {
|
||||
"en": "Very important news",
|
||||
"tr": "Çok önemli haber"
|
||||
},
|
||||
"author": "ngn",
|
||||
"content": {
|
||||
"en": "Just letting you know that I'm testing the API",
|
||||
"tr": "Sadece API'ı test ettiğimi bilmenizi istedim"
|
||||
}
|
||||
}
|
||||
```
|
||||
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)
|
||||
- `content`: Haber paylaşımının içerği (Multilang)
|
||||
|
||||
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.
|
||||
|
||||
Başarılı ise herhangi bir veri döndürmez.
|
4
doc/pages/license.en.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "source license",
|
||||
"desc": "Source code license"
|
||||
}
|