Compare commits

1 Commits

Author SHA1 Message Date
b5bae3e3e8 Update dependency @sveltejs/vite-plugin-svelte to v5
Some checks failed
renovate/artifacts Artifact file update failure
2025-01-20 07:02:31 +00:00
79 changed files with 732 additions and 688 deletions

View File

@ -3,7 +3,6 @@ name: Build the docker image for the API
on: on:
push: push:
branches: ["main"] branches: ["main"]
paths: ["api/**"]
env: env:
REGISTRY: git.ngn.tf REGISTRY: git.ngn.tf

View File

@ -3,7 +3,6 @@ name: Build the docker image for the frontend application
on: on:
push: push:
branches: ["main"] branches: ["main"]
paths: ["app/**"]
env: env:
REGISTRY: git.ngn.tf REGISTRY: git.ngn.tf
@ -28,8 +27,8 @@ jobs:
cd app cd app
docker build --build-arg WEBSITE_REPORT_URL=https://git.ngn.tf/ngn/website/issues/new \ docker build --build-arg WEBSITE_REPORT_URL=https://git.ngn.tf/ngn/website/issues/new \
--build-arg WEBSITE_SOURCE_URL=https://git.ngn.tf/ngn/website \ --build-arg WEBSITE_SOURCE_URL=https://git.ngn.tf/ngn/website \
--build-arg WEBSITE_APP_URL=https://ngn.tf \
--build-arg WEBSITE_API_URL=https://api.ngn.tf \
--build-arg WEBSITE_DOC_URL=http://doc:7003 \ --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 . --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest .
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest

View File

@ -3,7 +3,6 @@ name: Build the docker image for the doc server
on: on:
push: push:
branches: ["main"] branches: ["main"]
paths: ["doc/**"]
env: env:
REGISTRY: git.ngn.tf REGISTRY: git.ngn.tf

21
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,21 @@
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"

147
README.md
View File

@ -1,115 +1,86 @@
# website | my personal website # website | my personal website
![Version](https://img.shields.io/github/package-json/v/ngn13/ngn13.fun?filename=app%2Fpackage.json&color=blue)
![Website Status](https://img.shields.io/website?up_color=green&down_color=red&url=https%3A%2F%2Fngn.tf&label=Website%20Status)
![API Status](https://img.shields.io/website?up_color=green&down_color=red&url=https%3A%2F%2Fapi.ngn.tf&label=API%20Status)
![](https://git.ngn.tf/ngn/website/actions/workflows/build-app.yml/badge.svg) This repo contains all the source code for my personal website, [ngn.tf](https://ngn.tf)
![](https://git.ngn.tf/ngn/website/actions/workflows/build-api.yml/badge.svg) All code is licensed under AGPL version 3 (see [LICENSE.txt](LICENSE.txt))
![](https://git.ngn.tf/ngn/website/actions/workflows/build-doc.yml/badge.svg)
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 ## Directory structure
### `app` ### `app`
Contains frontend application, written with SvelteKit. It supports full SSR. Contains frontend application, written with SvelteKit. It supports full SSR.
Contains modified CSS from Contains modified CSS from [github-markdown-css](https://github.com/sindresorhus/github-markdown-css)
[github-markdown-css](https://github.com/sindresorhus/github-markdown-css) and and fonts from [NerdFonts](https://www.nerdfonts.com/)
fonts from [NerdFonts](https://www.nerdfonts.com/)
### `api` ### `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 [Gin](https://github.com/gin-gonic/gin)
(see history section).
Contains the API server, written in Go. It uses the API stores all the data in a local SQLite(3) database. Go doesn't support SQLite3 out of the box so
[Fiber](https://github.com/gofiber/fiber) web framework which offers an I'm using [mattn's sqlite3 driver](https://github.com/mattn/go-sqlite3).
[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).
### `doc` ### `doc`
Contains the documentation server, written in C. It uses the [ctorm](https://github.com/ngn13/ctorm) web
Contains the documentation server, written in C. It uses the framework, which is a framework that I myself wrote. Unlike the frontend application or the API server, it's not
[ctorm](https://github.com/ngn13/ctorm) web framework, which is a framework that accessable by public, the frontend application gets the documentation content from this server and renders it using
I myself wrote. Unlike the frontend application or the API server, it's not SSR. The reason I don't use the API for hosting the documentation content is that I want a separate server for hosting
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. static content, API is only for hosting dynamic stuff.
### `admin` ### `admin`
The frontend application does not contain an admin interface, I do the administration stuff (such as adding news posts,
The frontend application does not contain an admin interface, I do the adding services etc.) using the python script in this directory. This script can be installed on to the PATH by running
administration stuff (such as adding news posts, adding services etc.) using the the Makefile install script. After installation it can be used by running `admin_script`.
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 ## Deployment
Easiest way to deploy is to use docker. There is `compose.yml` and a `run.sh` script in the [deploy](deploy/) directory
Easiest way to deploy is to use docker. There is `compose.yml` and a `run.sh` that can be used to startup all the docker containers. Configuration options are passed during build time for the frontend
script in the [deploy](deploy/) directory that can be used to startup all the application, and for others it's passed with environment variables.
docker containers. Configuration options are passed during build time for the
frontend application, and for others it's passed with environment variables.
## History ## History
Some nostalgic history/changelog stuff (just for the major version numbers): 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 - **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was just a simple HTML/CSS page,
just a simple HTML/CSS page, I never published any of the source code and I I never published any of the source code and I wiped the local copy on my USB drive in early 2022, I still
wiped the local copy on my USB drive in early 2022, I still remember what it remember what it looked like though, it looked like I made entire website in microsoft paint... while blindfoled,
looked like though, it looked like I made entire website in microsoft paint... so yeah it was shit.
while blindfoled, so yeah it was shit.
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my - **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code
github.io page, and all the source code was (and still is) avaliable, it was was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png).
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 - **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework) my website with one
(and rework) my website with one of the fancy JS frameworks. I decided to go of the fancy JS frameworks. I decided to go with Svelte. Not the kit version, at the time svelte did not support SSR.
with Svelte. Not the kit version, at the time svelte did not support SSR. I do 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
not remember writting an API for it so I guess I just updated it everytime I much like a static website and was hosted on `ngn13.fun` as at this point I had my own hosting. The source code for
wanted to add content? It was pretty much like a static website and was hosted 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
on `ngn13.fun` as at this point I had my own hosting. The source code for this to find it. I also do not remember how it looked like, sooo this version is pretty much lost :(
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 - **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework my website one more
to rewrite and rework my website one more time in NuxtJS. I had really "fun" time in NuxtJS. I had really "fun" time using vue stuff. As NuxtJS supported server-side code, this website had its own
time using vue stuff. As NuxtJS supported server-side code, this website had built in API. This website was also hosted on `ngn13.fun`. This also the first version that lives on this git repository.
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 - **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite and rework my website again, this time
and rework my website again, this time I was going with SvelteKit as I haven't I was going with SvelteKit as I haven't had the greatest experience with NuxtJS. SvelteKit was really fun to work with
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
and I got my new website done pretty quickly. (I don't wanna brag or something 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
but I really imporeved the CSS/styling stuff ya know). I also wrote a new API 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.
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 - **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0, the big changes are in
still similar to 3.0, the big changes are in the API. I rewrote the API with the API. I rewrote the API with Fiber. This version was the first version hosted on `ngn.tf` which is my new domain name.
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 - 2025 january)**: This version just had simple frontend - **v5.0 (2024 october - 2025 january)**: This version just had simple frontend UI changes compared to 4.0, at this
UI changes compared to 4.0, at this point I was thinking about doing a massive point I was thinking about doing a massive rework (which I did with 6.0), however I was working on some other shit at
rework (which I did with 6.0), however I was working on some other shit at the the time, so I just did some small changes with the limited time I had for this project.
time, so I just did some small changes with the limited time I had for this
project.
- **v6.0 (2025 january - ...)**: The current major version of my website, - **v6.0 (2025 january - ...)**: The current major version of my website, frontend had a massive rework, API has been
frontend had a massive rework, API has been cleaned up and extended to do cleaned up and extended to do status checking for the services I host. The `doc` server has been added to the mix
status checking for the services I host. The `doc` server has been added to so I can host static documentation. The most important thing about this version is that it adds multi-language support,
the mix so I can host static documentation. The most important thing about so literally everything on the website (including the API and documentation content) is localized for both English
this version is that it adds multi-language support, so literally everything and Turkish, which was something I wanted to do for the longest time ever.
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 Damn it has been 4 years since I wrote that shit HTML page huh? Time flies...
time ever.
## Screenshots (from v4.0)
![](assets/4.0_index.png)
![](assets/4.0_blog.png)

View File

@ -218,7 +218,6 @@ class AdminScript:
"logs": self.get_logs, "logs": self.get_logs,
} }
self.api_url_env = "API_URL" self.api_url_env = "API_URL"
self.password_env = "API_PASSWORD"
def __format_time(self, ts: int) -> str: def __format_time(self, ts: int) -> str:
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y") return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
@ -265,10 +264,7 @@ class AdminScript:
return False return False
try: try:
password = getenv(self.password_env) password = self.log.password("Please enter the admin password")
if password is None:
password = self.log.password("Please enter the admin password")
self.api = AdminAPI(url, password) self.api = AdminAPI(url, password)
if len(argv) == 2: if len(argv) == 2:
@ -405,4 +401,4 @@ class AdminScript:
if __name__ == "__main__": if __name__ == "__main__":
script = AdminScript() script = AdminScript()
exit(0 if script.run() else 1) exit(script.run() if 1 else 0)

View File

@ -1,4 +1,4 @@
FROM golang:1.24.3 FROM golang:1.23.4
WORKDIR /api WORKDIR /api

View File

@ -3,47 +3,107 @@ package config
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"os"
"github.com/ngn13/ortam"
) )
type Type struct { type Type struct {
Debug bool // should display debug messgaes? Options []Option
AppUrl *url.URL // frontend application URL for the website Count int
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 Load() (*Type, error) { func (c *Type) Find(name string, typ uint8) (*Option, error) {
var conf = Type{ for i := 0; i < c.Count; i++ {
Debug: false, if c.Options[i].Name != name {
Password: "", continue
Host: "0.0.0.0:7002", }
IPHeader: "X-Real-IP",
Interval: "1h", if c.Options[i].Type != typ {
Timeout: "15s", return nil, fmt.Errorf("bad option type")
Limit: "5s", }
return &c.Options[i], nil
} }
if err := ortam.Load(&conf, "WEBSITE"); err != nil { return nil, fmt.Errorf("option not found")
return nil, err }
}
func (c *Type) Load() (err error) {
if conf.AppUrl == nil { var (
conf.AppUrl, _ = url.Parse("http://localhost:7001/") env_val string
} env_name string
opt *Option
if conf.Password == "" { exists bool
return nil, fmt.Errorf("password is not specified") )
}
// default options
if conf.Host == "" { c.Options = []Option{
return nil, fmt.Errorf("host address is not specified") {Name: "debug", Value: "false", Type: OPTION_TYPE_BOOL, Required: true}, // should display debug messgaes?
} {Name: "app_url", Value: "http://localhost:7001/", Type: OPTION_TYPE_URL, Required: true}, // frontend application URL for the website
{Name: "password", Value: "", Type: OPTION_TYPE_STR, Required: true}, // admin password
return &conf, nil {Name: "host", Value: "0.0.0.0:7002", Type: OPTION_TYPE_STR, Required: true}, // host the server should listen on
{Name: "ip_header", Value: "X-Real-IP", Type: OPTION_TYPE_STR, Required: false}, // header that should be checked for obtaining the client IP
{Name: "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 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
} }

49
api/config/option.go Normal file
View File

@ -0,0 +1,49 @@
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("WEBSITE_%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
}

View File

@ -1,23 +1,22 @@
module github.com/ngn13/website/api module github.com/ngn13/website/api
go 1.24.0 go 1.21.3
require ( require (
github.com/gofiber/fiber/v2 v2.52.8 github.com/gofiber/fiber/v2 v2.52.5
github.com/mattn/go-sqlite3 v1.14.28 github.com/mattn/go-sqlite3 v1.14.24
github.com/ngn13/ortam v0.0.0-20250421004351-8dea81680817
) )
require ( require (
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.5.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.15.0 // indirect
) )

View File

@ -1,28 +1,20 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24/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 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@ -33,5 +25,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= 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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@ -36,18 +36,18 @@ func main() {
app *fiber.App app *fiber.App
stat status.Type stat status.Type
conf *config.Type conf config.Type
db database.Type db database.Type
err error err error
) )
if conf, err = config.Load(); err != nil { if err = conf.Load(); err != nil {
util.Fail("failed to load the configuration: %s", err.Error()) util.Fail("failed to load the configuration: %s", err.Error())
return return
} }
if !conf.Debug { if !conf.GetBool("debug") {
util.Debg = func(m string, v ...any) {} util.Debg = func(m string, v ...any) {}
} }
@ -56,7 +56,7 @@ func main() {
return 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()) util.Fail("failed to setup the status checker: %s", err.Error())
return 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.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("status", &stat)
c.Locals("config", conf) c.Locals("config", &conf)
c.Locals("database", &db) c.Locals("database", &db)
return c.Next() return c.Next()
@ -121,9 +121,9 @@ func main() {
} }
// start the app // start the app
util.Info("starting web server on %s", conf.Host) util.Info("starting web server on %s", conf.GetStr("host"))
if err = app.Listen(conf.Host); err != nil { if err = app.Listen(conf.GetStr("host")); err != nil {
util.Fail("failed to start the web server: %s", err.Error()) util.Fail("failed to start the web server: %s", err.Error())
} }

View File

@ -21,7 +21,7 @@ func admin_log(c *fiber.Ctx, m string) error {
func AuthMiddleware(c *fiber.Ctx) error { func AuthMiddleware(c *fiber.Ctx) error {
conf := c.Locals("config").(*config.Type) conf := c.Locals("config").(*config.Type)
if c.Get("Authorization") != conf.Password { if c.Get("Authorization") != conf.GetStr("password") {
return util.ErrAuth(c) return util.ErrAuth(c)
} }

View File

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

View File

@ -40,6 +40,7 @@ func GET_News(c *fiber.Ctx) error {
db := c.Locals("database").(*database.Type) db := c.Locals("database").(*database.Type)
conf := c.Locals("config").(*config.Type) conf := c.Locals("config").(*config.Type)
app := conf.GetURL("app_url")
lang := c.Params("lang") lang := c.Params("lang")
if lang == "" || len(lang) != 2 { if lang == "" || len(lang) != 2 {
@ -62,10 +63,10 @@ func GET_News(c *fiber.Ctx) error {
}) })
if feed, err = util.Render("views/news.xml", fiber.Map{ if feed, err = util.Render("views/news.xml", fiber.Map{
"app_url": conf.AppUrl,
"updated": time.Now().Format(time.RFC3339), "updated": time.Now().Format(time.RFC3339),
"entries": entries, "entries": entries,
"lang": lang, "lang": lang,
"app": app,
}); err != nil { }); err != nil {
return util.ErrInternal(c, err) return util.ErrInternal(c, err)
} }

View File

@ -66,24 +66,29 @@ func (s *Type) loop() {
func (s *Type) Setup(conf *config.Type, db *database.Type) error { func (s *Type) Setup(conf *config.Type, db *database.Type) error {
var ( var (
dur time.Duration dur time.Duration
err error iv, to, lm string
err error
) )
if conf.Interval == "" || conf.Timeout == "" || conf.Limit == "" { iv = conf.GetStr("interval")
to = conf.GetStr("timeout")
lm = conf.GetStr("limit")
if iv == "" || to == "" || lm == "" {
s.disabled = true s.disabled = true
return nil return nil
} }
if dur, err = util.GetDuration(conf.Interval); err != nil { if dur, err = util.GetDuration(iv); err != nil {
return err return err
} }
if s.timeout, err = util.GetDuration(conf.Timeout); err != nil { if s.timeout, err = util.GetDuration(iv); err != nil {
return err return err
} }
if s.limit, err = util.GetDuration(conf.Limit); err != nil { if s.limit, err = util.GetDuration(iv); err != nil {
return err return err
} }

View File

@ -10,9 +10,10 @@ import (
func IP(c *fiber.Ctx) string { func IP(c *fiber.Ctx) string {
conf := c.Locals("config").(*config.Type) conf := c.Locals("config").(*config.Type)
ip_header := conf.GetStr("ip_header")
if conf.IPHeader != "" && c.Get(conf.IPHeader) != "" { if ip_header != "" && c.Get(ip_header) != "" {
return strings.Clone(c.Get(conf.IPHeader)) return strings.Clone(c.Get(ip_header))
} }
return c.IP() return c.IP()

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom"> <?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
<title>{{.app_url.Host}} news</title> <title>{{.app.Host}} news</title>
<updated>{{.updated}}</updated> <updated>{{.updated}}</updated>
<subtitle>News and updates about my projects and self-hosted services</subtitle> <subtitle>News and updates about my projects and self-hosted services</subtitle>
<link href="{{.app_url.JoinPath "/news"}}"></link> <link href="{{.app.JoinPath "/news"}}"></link>
{{ range .entries }} {{ range .entries }}
<entry> <entry>
<title>{{.Title}}</title> <title>{{.Title}}</title>

1
app/.gitignore vendored
View File

@ -8,4 +8,3 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
nerdfonts.*

View File

@ -1,24 +1,22 @@
# build the application with node # build the application with node
FROM node:23.11.1 AS build FROM node:23.5.0 AS build
ARG WEBSITE_REPORT_URL ARG WEBSITE_REPORT_URL
ARG WEBSITE_SOURCE_URL ARG WEBSITE_SOURCE_URL
ARG WEBSITE_DOC_URL ARG WEBSITE_APP_URL
ARG WEBSITE_API_URL ARG WEBSITE_API_URL
ARG WEBSITE_API_PATH ARG WEBSITE_DOC_URL
ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL
ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL
ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL ENV WEBSITE_APP_URL=$WEBSITE_APP_URL
ENV WEBSITE_API_URL=$WEBSITE_API_URL ENV WEBSITE_API_URL=$WEBSITE_API_URL
ENV WEBSITE_API_PATH=$WEBSITE_API_PATH ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN apt install -y make sed wget RUN npm install && npm run build
RUN npm install
RUN make
# run it with bun (a lot faster) # run it with bun (a lot faster)
FROM oven/bun:latest AS main FROM oven/bun:latest AS main

View File

@ -1,25 +1,10 @@
NF_CSS = static/css/nerdfonts.css all:
NF_WOFF = static/assets/nerdfonts.woff2
all: $(NF_CSS)
npm run build 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: format:
npm run format npm run format
clean: run:
rm -rf build npm run dev
rm $(NF_CSS)
rm $(NF_WOFF)
.PHONY: format run clean .PHONY: format

168
app/package-lock.json generated
View File

@ -1,19 +1,20 @@
{ {
"name": "website", "name": "website",
"version": "6.3", "version": "6.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "website", "name": "website",
"version": "6.3", "version": "6.0",
"dependencies": { "dependencies": {
"@types/dompurify": "^3.2.0",
"dompurify": "^3.2.3", "dompurify": "^3.2.3",
"marked": "^15.0.6", "marked": "^15.0.6",
"svelte-i18n": "^4.0.1" "svelte-i18n": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.11", "@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.15.1", "@sveltejs/kit": "^2.15.1",
"@sveltejs/vite-plugin-svelte": "^4.0.3", "@sveltejs/vite-plugin-svelte": "^4.0.3",
@ -831,31 +832,23 @@
"win32" "win32"
] ]
}, },
"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": { "node_modules/@sveltejs/adapter-auto": {
"version": "6.0.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz",
"integrity": "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==", "integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==",
"dev": true, "dev": true,
"license": "MIT", "dependencies": {
"import-meta-resolve": "^4.1.0"
},
"peerDependencies": { "peerDependencies": {
"@sveltejs/kit": "^2.0.0" "@sveltejs/kit": "^2.0.0"
} }
}, },
"node_modules/@sveltejs/adapter-node": { "node_modules/@sveltejs/adapter-node": {
"version": "5.2.12", "version": "5.2.11",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.11.tgz",
"integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", "integrity": "sha512-lR7/dfUaKFf3aI408KRDy/BVDYoqUws7zNOJz2Hl4JoshlTnMgdha3brXBRFXB+cWtYvJjjPhvmq3xqpbioi4w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
@ -867,25 +860,24 @@
} }
}, },
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.21.3", "version": "2.15.1",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.21.3.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.15.1.tgz",
"integrity": "sha512-Bd05srNOaqP05qnytjg/KkWNlkcwEpE76s0xGSlgzL4I8pLyrK3c9+a7zMCquoiEEIZF2ecGTn6Fj/lELjaa8A==", "integrity": "sha512-8t7D3hQHbUDMiaQ2RVnjJJ/+Ur4Fn/tkeySJCsHtX346Q9cp3LAnav8xXdfuqYNJwpUGX0x3BqF1uvbmXQw93A==",
"dev": true, "dev": true,
"license": "MIT", "hasInstallScript": true,
"dependencies": { "dependencies": {
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"acorn": "^8.14.1",
"cookie": "^0.6.0", "cookie": "^0.6.0",
"devalue": "^5.1.0", "devalue": "^5.1.0",
"esm-env": "^1.2.2", "esm-env": "^1.2.1",
"import-meta-resolve": "^4.1.0",
"kleur": "^4.1.5", "kleur": "^4.1.5",
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
"mrmime": "^2.0.0", "mrmime": "^2.0.0",
"sade": "^1.8.1", "sade": "^1.8.1",
"set-cookie-parser": "^2.6.0", "set-cookie-parser": "^2.6.0",
"sirv": "^3.0.0", "sirv": "^3.0.0",
"vitefu": "^1.0.6" "tiny-glob": "^0.2.9"
}, },
"bin": { "bin": {
"svelte-kit": "svelte-kit.js" "svelte-kit": "svelte-kit.js"
@ -900,11 +892,10 @@
} }
}, },
"node_modules/@sveltejs/vite-plugin-svelte": { "node_modules/@sveltejs/vite-plugin-svelte": {
"version": "4.0.4", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.3.tgz",
"integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==", "integrity": "sha512-J7nC5gT5qpmvyD2pmzPUntLUgoinyEaNy9sTpGGE6N7pblggO0A1NyneJJvR2ELlzK6ti28aF2SLXG1yJdnJeA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0", "@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0",
"debug": "^4.3.7", "debug": "^4.3.7",
@ -921,12 +912,11 @@
"vite": "^5.0.0" "vite": "^5.0.0"
} }
}, },
"node_modules/@sveltejs/vite-plugin-svelte-inspector": { "node_modules/@sveltejs/vite-plugin-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "3.0.1", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.0.tgz",
"integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==", "integrity": "sha512-hBxSYW/66989cq9dN248omD/ziskSdIV1NqfuueuAI1z6jGcg14k9Zd98pDIEnoA6wC9kWUGuQ6adzBbWwQyRg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.3.7" "debug": "^4.3.7"
}, },
@ -946,6 +936,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/estree": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -965,10 +964,9 @@
"optional": true "optional": true
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.1", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -976,6 +974,14 @@
"node": ">=0.4.0" "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": { "node_modules/aria-query": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@ -1084,9 +1090,9 @@
"dev": true "dev": true
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.2.6", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
"license": "(MPL-2.0 OR Apache-2.0)", "license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": { "optionalDependencies": {
"@types/trusted-types": "^2.0.7" "@types/trusted-types": "^2.0.7"
@ -1183,10 +1189,9 @@
} }
}, },
"node_modules/esm-env": { "node_modules/esm-env": {
"version": "1.2.2", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng=="
"license": "MIT"
}, },
"node_modules/esniff": { "node_modules/esniff": {
"version": "2.0.1", "version": "2.0.1",
@ -1204,10 +1209,9 @@
} }
}, },
"node_modules/esrap": { "node_modules/esrap": {
"version": "1.4.6", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz", "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz",
"integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", "integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==",
"license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15"
} }
@ -1299,6 +1303,17 @@
"node": ">= 0.4" "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": { "node_modules/intl-messageformat": {
"version": "10.7.11", "version": "10.7.11",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.11.tgz", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.11.tgz",
@ -1385,9 +1400,9 @@
} }
}, },
"node_modules/marked": { "node_modules/marked": {
"version": "15.0.12", "version": "15.0.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"marked": "bin/marked.js" "marked": "bin/marked.js"
@ -1516,9 +1531,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.5.3", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@ -1532,9 +1547,9 @@
} }
}, },
"node_modules/prettier-plugin-svelte": { "node_modules/prettier-plugin-svelte": {
"version": "3.4.0", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.2.tgz",
"integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "integrity": "sha512-kRPjH8wSj2iu+dO+XaUv4vD8qr5mdDmlak3IT/7AOgGIMRG86z/EHOLauFcClKEnOUf4A4nOA7sre5KrJD4Raw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
@ -1650,21 +1665,20 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "5.33.13", "version": "5.16.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.13.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.16.0.tgz",
"integrity": "sha512-uT3BAPpHGaJqpOgdwJwIK7P4JkBkSS0vylbaRXxQjt1gr+DZ9BiPkhmbZw3ql8LJofUyz5XyrzzQDgQQdfP86Q==", "integrity": "sha512-Ygqsiac6UogVED2ruKclU+pOeMThxWtp9LG+li7BXeDKC2paVIsRTMkNmcON4Zejerd1s5sZHWx6ZtU85xklVg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.3.0", "@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5", "@types/estree": "^1.0.5",
"acorn": "^8.12.1", "acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1", "aria-query": "^5.3.1",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"esm-env": "^1.2.1", "esm-env": "^1.2.1",
"esrap": "^1.4.6", "esrap": "^1.3.2",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",
@ -2149,11 +2163,10 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.19", "version": "5.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
@ -2209,17 +2222,12 @@
} }
}, },
"node_modules/vitefu": { "node_modules/vitefu": {
"version": "1.0.6", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.3.tgz",
"integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", "integrity": "sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==",
"dev": true, "dev": true,
"license": "MIT",
"workspaces": [
"tests/deps/*",
"tests/projects/*"
],
"peerDependencies": { "peerDependencies": {
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"vite": { "vite": {

View File

@ -1,6 +1,6 @@
{ {
"name": "website", "name": "website",
"version": "6.3", "version": "6.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@ -10,10 +10,10 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.11", "@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.15.1", "@sveltejs/kit": "^2.15.1",
"@sveltejs/vite-plugin-svelte": "^4.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2", "prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.16.0", "svelte": "^5.16.0",

View File

@ -1,14 +1,9 @@
import { browser } from "$app/environment";
import { urljoin } from "$lib/util.js"; import { urljoin } from "$lib/util.js";
const api_version = "v1"; const api_version = "v1";
const api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version);
function api_urljoin(path = null, query = {}) { function api_urljoin(path = null, query = {}) {
let api_url = "";
if (browser) api_url = urljoin(import.meta.env.WEBSITE_API_PATH, api_version);
else api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version);
return urljoin(api_url, path, query); return urljoin(api_url, path, query);
} }

View File

@ -1,36 +1,43 @@
<script> <script>
import { color, date_from_ts } from "$lib/util.js"; import { urljoin, color, date_from_ts } from "$lib/util.js";
import { api_get_metrics } from "$lib/api.js"; import { api_get_metrics } from "$lib/api.js";
import Link from "$lib/link.svelte"; import Link from "$lib/link.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
let show_counter = false, let data = {};
data = {};
onMount(async () => { onMount(async () => {
show_counter = true;
data = await api_get_metrics(fetch); data = await api_get_metrics(fetch);
}); });
</script> </script>
<footer style="border-top: solid 2px var(--{color()});"> <footer style="border-top: solid 2px var(--{color()});">
<div class="links"> <div class="info">
<div class="links">
<span>
<Link link={import.meta.env.WEBSITE_SOURCE_URL} bold={true}>{$_("footer.source")}</Link>
</span>
<span>/</span>
<span>
<Link link={urljoin(import.meta.env.WEBSITE_APP_URL, "doc/license")} bold={true}
>{$_("footer.license")}</Link
>
</span>
<span>/</span>
<span>
<Link link={urljoin(import.meta.env.WEBSITE_APP_URL, "doc/privacy")} bold={true}
>{$_("footer.privacy")}</Link
>
</span>
</div>
<span> <span>
<Link link={import.meta.env.WEBSITE_SOURCE_URL} bold={true}>{$_("footer.source")}</Link> {$_("footer.powered")}
</span>
<span>/</span>
<span>
<Link link="/doc/license" bold={true}>{$_("footer.license")}</Link>
</span>
<span>/</span>
<span>
<Link link="/doc/privacy" bold={true}>{$_("footer.privacy")}</Link>
</span> </span>
</div> </div>
{#if show_counter} <div class="useless">
<span class="counter"> <span>
{$_("footer.number", { {$_("footer.number", {
values: { values: {
total: data.total, total: data.total,
@ -41,9 +48,10 @@
<span style="color: var(--{color()})">({$_("footer.wow")})</span> <span style="color: var(--{color()})">({$_("footer.wow")})</span>
{/if} {/if}
</span> </span>
{:else} <span>
<span class="counter">{$_("footer.js")}</span> {$_("footer.version", { values: { api_version: "v1", frontend_version: pkg.version } })}
{/if} </span>
</div>
</footer> </footer>
<style> <style>
@ -53,29 +61,27 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: var(--black-1); background: var(--black-1);
box-sizing: border-box;
padding: 20px 50px 20px 50px;
} }
div { div {
display: flex; display: flex;
color: var(--white-2);
font-size: var(--size-2); font-size: var(--size-2);
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
} }
span { .useless {
color: var(--white-2); margin: 25px 50px 25px 0;
font-size: 15px;
}
.counter {
text-align: right; text-align: right;
} }
.links { .info {
margin: 25px 0 25px 50px;
text-align: left; text-align: left;
}
.info .links {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 5px; gap: 5px;

View File

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

View File

@ -1,46 +1,15 @@
<script> <script>
import { browser } from "$app/environment";
import { color } from "$lib/util.js"; import { color } from "$lib/util.js";
import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
export let picture = ""; export let picture = "";
export let title = ""; export let title = "";
let title_cur = title;
let show_animation = false;
function animate(title) {
if (!browser) return;
let id = window.setTimeout(function () {}, 0);
while (id--) clearTimeout(id);
title_cur = "";
for (let i = 0; i < title.length; i++) {
setTimeout(() => {
title_cur += title[i];
}, i * 70);
}
}
onMount(() => {
show_animation = true;
});
$: animate(title);
</script> </script>
<header> <header>
<div> <div>
{#if show_animation} <h1 class="title" style="color: var(--{color()})">{title.toLowerCase()}</h1>
<h1 class="title" style="color: var(--{color()})">{title_cur}</h1> <h1 class="cursor" style="color: var(--{color()})">_</h1>
<h1 class="cursor" style="color: var(--{color()})">_</h1>
{:else}
<h1 class="title" style="color: var(--{color()})">{title}</h1>
{/if}
</div> </div>
<img src="/profile/{picture}.png" alt="" /> <img src="/profile/{picture}.png" alt="" />
</header> </header>
@ -81,6 +50,9 @@
header div .title { header div .title {
text-shadow: var(--text-shadow); text-shadow: var(--text-shadow);
overflow: hidden; overflow: hidden;
width: 0;
animation: typing 1s steps(20, end) forwards;
animation-delay: 0.3s;
} }
header div .cursor { header div .cursor {

View File

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

View File

@ -19,18 +19,28 @@ function color() {
} }
function click() { function click() {
let audio = new Audio("/assets/click.wav"); let audio = new Audio("/click.wav");
audio.play(); audio.play();
} }
function urljoin(url, path = null) { function urljoin(url, path = null, query = {}) {
if (undefined === url || null === url) return; if (undefined === url || null === url) return;
if (url[url.length - 1] != "/") url += "/"; let url_len = url.length;
if (null === path || "" === path) return url; if (url[url_len - 1] != "/") url += "/";
if (path[0] === "/") return url + path.slice(1);
return url + path; if (null === path || "" === path) url = new URL(url);
else if (path[0] === "/") url = new URL(path.slice(1), url);
else url = new URL(path, url);
for (let k in query) url.searchParams.append(k, query[k]);
return url.href;
}
function app_url(path = null, query = {}) {
return urljoin(import.meta.env.WEBSITE_APP_URL, path, query);
} }
function time_from_ts(ts) { function time_from_ts(ts) {
@ -58,4 +68,4 @@ function date_from_ts(ts) {
}).format(new Date(ts * 1000)); }).format(new Date(ts * 1000));
} }
export { color, click, urljoin, time_from_ts, date_from_ts }; export { color, click, urljoin, app_url, time_from_ts, date_from_ts };

View File

@ -5,7 +5,7 @@
"donate": "donate" "donate": "donate"
}, },
"home": { "home": {
"title": "hello world!", "title": "Hello world!",
"welcome": { "welcome": {
"title": "about", "title": "about",
"desc": "Welcome to my website, I'm ngn", "desc": "Welcome to my website, I'm ngn",
@ -29,9 +29,9 @@
}, },
"services": { "services": {
"title": "services", "title": "services",
"desc": "A part from working on stupid shit, I host free (as in freedom and price) services available for all", "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", "speed": "All of these services are available over a 600 Mbit/s interface",
"security": "All use SSL encrypted connection and they respect your privacy and freedom", "security": "All use SSL encrypted connection and they are all privacy-respecting",
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks", "privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
"bullshit": "No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit", "bullshit": "No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit",
"link": "See all the services!" "link": "See all the services!"
@ -42,7 +42,7 @@
} }
}, },
"services": { "services": {
"title": "service status", "title": "Service Status",
"none": "No services found", "none": "No services found",
"search": "Search for a service", "search": "Search for a service",
"feed": "News and updates", "feed": "News and updates",
@ -54,7 +54,7 @@
} }
}, },
"donate": { "donate": {
"title": "donate!", "title": "Donate Money!",
"info": "I spend a lot of time and money on different projects and maintaining different services.", "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).", "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.", "details": "So even a small donation would be useful. And it would help me keep everything up and running.",
@ -64,16 +64,20 @@
"address": "Adress/Link" "address": "Adress/Link"
} }
}, },
"doc": {
"title": "Documentation"
},
"error": { "error": {
"title": "something went wrong!", "title": "Something went wrong!",
"report": "Report this issue" "report": "Report this issue"
}, },
"footer": { "footer": {
"source": "Source", "source": "Source",
"license": "License", "license": "License",
"privacy": "Privacy", "privacy": "Privacy",
"powered": "Powered by Svelte, Go, SQLite and donations",
"number": "Visited {total} times since {since}", "number": "Visited {total} times since {since}",
"wow": "wow!!", "wow": "wow!!",
"js": "Enable javascript to display all the elements" "version": "Using API version {api_version}, frontend version {frontend_version}"
} }
} }

View File

@ -5,7 +5,7 @@
"donate": "bağış" "donate": "bağış"
}, },
"home": { "home": {
"title": "merhaba dünya!", "title": "Merhaba Dünya!",
"welcome": { "welcome": {
"title": "hakkımda", "title": "hakkımda",
"desc": "Websiteme hoşgeldiniz, ben ngn", "desc": "Websiteme hoşgeldiniz, ben ngn",
@ -30,10 +30,11 @@
"services": { "services": {
"title": "servisler", "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", "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", "speed": "Tüm servisler 600 Mbit/s 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", "security": "Hepsi SSL şifreli bağlantı kullanıyor ve hepsi gizliğinize önem veriyor",
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
"privacy": "Açık ağdan, TOR ve I2P'den erişilebilirler, bölge ya da ağ blokları yok", "privacy": "Açık ağdan, TOR ve I2P'den erişilebilirler, bölge ya da ağ blokları yok",
"bullshit": "CDN yok, cloudflare yok, CAPTCHA yok, analitikler ve diğer saçmalıklar yok", "bullshit": "CDN yok, cloudflare yok, CAPTCHA yok, analitikler yok, boktan saçmalıklar yok",
"link": "Tüm servisleri incele!" "link": "Tüm servisleri incele!"
}, },
"projects": { "projects": {
@ -42,7 +43,7 @@
} }
}, },
"services": { "services": {
"title": "servis durumu", "title": "Servis Durumu",
"none": "Servis bulunamadı", "none": "Servis bulunamadı",
"search": "Bir servisi ara", "search": "Bir servisi ara",
"feed": "Yenilikler ve güncellemeler", "feed": "Yenilikler ve güncellemeler",
@ -54,7 +55,7 @@
} }
}, },
"donate": { "donate": {
"title": "bağış yap!", "title": "Para Bağışla!",
"info": "Farklı projeler ve farklı servisleri yönetmek için oldukça zaman ve para harcıyorum.", "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$).", "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.", "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.",
@ -64,16 +65,20 @@
"address": "Adres/Bağlantı" "address": "Adres/Bağlantı"
} }
}, },
"doc": {
"title": "Dökümantasyon"
},
"error": { "error": {
"title": "birşeyler yanlış gitti!", "title": "Birşeyler yanlış gitti!",
"report": "Bu sorunu raporlayın" "report": "Bu sorunu raporlayın"
}, },
"footer": { "footer": {
"source": "Kaynak", "source": "Kaynak",
"license": "Lisans", "license": "Lisans",
"privacy": "Gizlilik", "privacy": "Gizlilik",
"powered": "Svelte, Go, SQLite ve bağışlar tarafından destekleniyor",
"number": "{since} tarihinden beri {total} kez ziyaret edildi", "number": "{since} tarihinden beri {total} kez ziyaret edildi",
"wow": "vay be!!", "wow": "vay be!!",
"js": "Tüm elementleri görüntelemek için javascript'i açın" "version": "Kullan API versiyonu {api_version}, arayüz versiyonu {frontend_version}"
} }
} }

View File

@ -21,7 +21,7 @@
</main> </main>
<style> <style>
@import "/css/global.css"; @import "/global.css";
main { main {
display: flex; display: flex;

View File

@ -1,22 +1,14 @@
<script> <script>
import { api_version } from "$lib/api.js";
import Header from "$lib/header.svelte"; import Header from "$lib/header.svelte";
import Error from "$lib/error.svelte"; import Error from "$lib/error.svelte";
import Head from "$lib/head.svelte"; import Head from "$lib/head.svelte";
import Card from "$lib/card.svelte"; import Card from "$lib/card.svelte";
import Link from "$lib/link.svelte"; import Link from "$lib/link.svelte";
import { browser } from "$app/environment";
import { _, locale } from "svelte-i18n"; import { _, locale } from "svelte-i18n";
import { color } from "$lib/util.js"; import { color } from "$lib/util.js";
let { data } = $props(); let { data } = $props();
if (browser) {
window._version = {};
window._version.app = pkg.version;
window._version.api = api_version;
}
</script> </script>
<Head title="home" desc="home page of my personal website" /> <Head title="home" desc="home page of my personal website" />

View File

@ -1,8 +1,9 @@
import { doc_get } from "$lib/doc"; import { doc_get_list, doc_get } from "$lib/doc";
export async function load({ fetch, params }) { export async function load({ fetch, params }) {
try { try {
return { return {
docs: await doc_get_list(fetch),
doc: await doc_get(fetch, params.name), doc: await doc_get(fetch, params.name),
error: "", error: "",
}; };

View File

@ -22,7 +22,7 @@
</script> </script>
<Head title="documentation" desc="website and API documentation" /> <Head title="documentation" desc="website and API documentation" />
<Header picture="reader" title={data.doc[$locale].title} /> <Header picture="reader" title={$_("doc.title")} />
{#if data.error.length !== 0} {#if data.error.length !== 0}
{#if !data.error.includes("not found")} {#if !data.error.includes("not found")}
@ -30,21 +30,94 @@
{/if} {/if}
{:else} {:else}
<main> <main>
<div class="markdown-body" style="--link-color: var(--{color()})"> {#if data.doc !== undefined}
{@html marked.parse(data.doc[$locale].content)} <div class="markdown-body" style="--link-color: var(--{color()})">
</div> {@html marked.parse(data.doc[$locale].content)}
</div>
<div class="docs">
{#each data.docs[$locale] as doc}
{#if doc.title == data.doc[$locale].title}
<a href="/doc/{doc.name}" style="border-color: var(--{color()})">
<h1>{doc.title}</h1>
<h3>{doc.desc}</h3>
</a>
{:else}
<a href="/doc/{doc.name}" style="border-color: var(--white-3)">
<h1>{doc.title}</h1>
<h3>{doc.desc}</h3>
</a>
{/if}
{/each}
</div>
{/if}
</main> </main>
{/if} {/if}
<style> <style>
@import "/css/markdown.css"; @import "/markdown.css";
main { main {
padding: 50px; padding: 50px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: start;
gap: 30px; gap: 30px;
} }
main .docs {
display: flex;
flex-direction: column;
align-items: end;
gap: 6px;
}
main .docs a {
display: flex;
flex-direction: column;
background: var(--black-3);
text-decoration: none;
box-sizing: border-box;
border-right-style: solid;
padding: 15px;
width: 100%;
gap: 4px;
}
main .docs a:hover {
box-shadow: var(--box-shadow-2);
}
main .docs a h1 {
font-size: var(--size-3);
color: var(--white-1);
font-weight: 900;
}
main .docs a h3 {
font-size: var(--size-2);
color: var(--white-3);
font-weight: 100;
text-decoration: none;
}
main .markdown-body :global(a) { main .markdown-body :global(a) {
color: var(--link-color); color: var(--link-color);
} }
@media only screen and (max-width: 900px) {
main {
flex-direction: column-reverse;
}
main .docs {
width: 100%;
}
main .docs a {
border-right-style: none;
border-left-style: solid;
width: 100%;
}
}
</style> </style>

View File

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

View File

@ -9,3 +9,13 @@
border-color: transparent; border-color: transparent;
} }
} }
@keyframes typing {
from {
width: 0%;
}
to {
width: 100%;
}
}

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

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

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

@ -0,0 +1,10 @@
/* 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");
}

View File

@ -1,12 +1,4 @@
/*
* 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 "./animations.css";
@import "./nerdfonts.css";
@import "./font.css"; @import "./font.css";
:root { :root {
@ -39,8 +31,7 @@
rgba(20, 20, 20, 0.3) 0px 18px 36px -18px inset; rgba(20, 20, 20, 0.3) 0px 18px 36px -18px inset;
--text-shadow: 3px 2px 8px rgba(50, 50, 50, 0.8); --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)), --background: linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96)), url("/banner.png");
url("/assets/banner.png");
--profile-size: 220px; --profile-size: 220px;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

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

View File

@ -3,38 +3,21 @@ import { defineConfig } from "vite";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
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 = { const default_env = {
source_url: "https://git.ngn.tf/ngn/website", REPORT_URL: "https://github.com/ngn13/website/issues",
report_url: "https://git.ngn.tf/ngn/website/issues", SOURCE_URL: "https://github.com/ngn13/website",
doc_url: "http://localhost:7003", APP_URL: "http://localhost:7001",
api: { API_URL: "http://localhost:7002",
url: "http://localhost:7002", DOC_URL: "http://localhost:7003",
path: "http://localhost:7002",
},
}; };
const package_file = fileURLToPath(new URL("package.json", import.meta.url)); const file = fileURLToPath(new URL("package.json", import.meta.url));
const package_json = readFileSync(package_file, "utf8"); const json = readFileSync(file, "utf8");
const package_data = JSON.parse(package_json); const pkg = JSON.parse(json);
env_from("WEBSITE", default_env); for (let env in default_env) {
if (process.env["WEBSITE_" + env] === undefined) process.env["WEBSITE_" + env] = default_env[env];
}
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()],
@ -48,6 +31,6 @@ export default defineConfig({
strictPort: true, strictPort: true,
}, },
define: { define: {
pkg: package_data, pkg: pkg,
}, },
}); });

View File

@ -7,20 +7,19 @@ services:
args: args:
WEBSITE_SOURCE_URL: "http://github.com/ngn13/website" WEBSITE_SOURCE_URL: "http://github.com/ngn13/website"
WEBSITE_REPORT_URL: "http://github.com/ngn13/website/issues" WEBSITE_REPORT_URL: "http://github.com/ngn13/website/issues"
WEBSITE_APP_URL: "http://localhost:7001"
WEBSITE_API_URL: "http://localhost:7002"
WEBSITE_DOC_URL: "http://doc:7003" WEBSITE_DOC_URL: "http://doc:7003"
WEBSITE_API_URL: "http://api:7002"
WEBSITE_API_PATH: "http://localhost:7002"
security_opt: security_opt:
- "no-new-privileges:true" - "no-new-privileges:true"
cap_drop: cap_drop:
- ALL - ALL
ports: ports:
- "127.0.0.1:7001:7001" - "127.0.0.1:7001:7001"
restart: unless-stopped
depends_on: depends_on:
- api - api
- doc - doc
read_only: true
restart: unless-stopped
api: api:
container_name: "website_api" container_name: "website_api"
@ -35,28 +34,28 @@ services:
- "127.0.0.1:7002:7002" - "127.0.0.1:7002:7002"
volumes: volumes:
- ./data.db:/api/data.db:rw - ./data.db:/api/data.db:rw
restart: unless-stopped
environment: environment:
WEBSITE_DEBUG: "false" WEBSITE_DEBUG: "false"
WEBSITE_APP_URL: "http://localhost:7001" WEBSITE_APP_URL: "http://localhost:7001/"
WEBSITE_PASSWORD: "change_me" WEBSITE_PASSWORD: "change_me"
WEBSITE_HOST: "0.0.0.0:7002" WEBSITE_HOST: "0.0.0.0:7002"
WEBSITE_IP_HEADER: "X-Real-IP" WEBSITE_IP_HEADER: "X-Real-IP"
WEBSITE_INTERVAL: "1h" WEBSITE_INTERVAL: "1h"
WEBSITE_TIMEOUT: "15s" WEBSITE_TIMEOUT: "15s"
WEBSITE_LIMIT: "5s" WEBSITE_LIMIT: "5s"
restart: unless-stopped
doc: doc:
container_name: "website_doc" container_name: "website_doc"
image: website_doc image: website_doc
read_only: true
build: build:
context: ./doc context: ./doc
security_opt: security_opt:
- "no-new-privileges:true" - "no-new-privileges:true"
cap_drop: cap_drop:
- ALL - ALL
restart: unless-stopped
environment: environment:
WEBSITE_HOST: "0.0.0.0:7003" WEBSITE_HOST: "0.0.0.0:7003"
WEBSITE_DOCS_DIR: "./docs" WEBSITE_DOCS_DIR: "./docs"
read_only: true
restart: unless-stopped

View File

@ -81,7 +81,7 @@ BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon BreakInheritanceList: BeforeColon
BreakStringLiterals: true BreakStringLiterals: true
ColumnLimit: 80 ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:' CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false CompactNamespaces: false
ConstructorInitializerIndentWidth: 4 ConstructorInitializerIndentWidth: 4

View File

@ -1,35 +0,0 @@
---
# "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
...

View File

@ -1,9 +1,9 @@
FROM ghcr.io/ngn13/ctorm:1.8.1 FROM ghcr.io/ngn13/ctorm:1.7
WORKDIR /doc WORKDIR /doc
COPY Makefile ./ COPY Makefile ./
COPY pages ./pages COPY docs ./docs
COPY inc ./inc COPY inc ./inc
COPY src ./src COPY src ./src

View File

@ -26,13 +26,10 @@ $(DISTDIR)/%.o: src/%.c
format: format:
clang-format -i -style=file $(CSRCS) $(HSRCS) clang-format -i -style=file $(CSRCS) $(HSRCS)
lint:
clang-tidy --warnings-as-errors --config= $(CSRCS) $(HSRCS)
clean: clean:
rm -rf $(DISTDIR) rm -rf $(DISTDIR)
run: run:
./doc.elf ./doc.elf
.PHONY: format lint clean run .PHONY: format clean run

View File

@ -1,4 +1,4 @@
{ {
"title": "API", "title": "API documentation",
"desc": "Website's API documentation" "desc": "Website's API documentation"
} }

View File

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

View File

@ -1,4 +1,4 @@
{ {
"title": "API", "title": "API dökümantasyonu",
"desc": "Websitesinin API dökümantasyonu" "desc": "Websitesinin API dökümantasyonu"
} }

View File

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

View File

@ -1,4 +1,4 @@
{ {
"title": "source license", "title": "License",
"desc": "Source code license" "desc": "Source code license"
} }

View File

@ -1,4 +1,4 @@
{ {
"title": "kaynak lisansı", "title": "Lisans",
"desc": "Kaynak kodu lisansı" "desc": "Kaynak kodu lisansı"
} }

4
doc/docs/privacy.en.json Normal file
View File

@ -0,0 +1,4 @@
{
"title": "Privacy",
"desc": "Learn how I respect your privacy"
}

4
doc/docs/privacy.tr.json Normal file
View File

@ -0,0 +1,4 @@
{
"title": "Gizlilik",
"desc": "Gizliliğinize nasıl önem verdiğimi öğrenin"
}

View File

@ -3,13 +3,13 @@
#include <stdbool.h> #include <stdbool.h>
#include <dirent.h> #include <dirent.h>
#include "file.h" #include "util.h"
typedef struct { typedef struct {
DIR *dir; DIR *dir;
file_t *file; util_file_t *file;
char name[NAME_MAX + 1]; char name[NAME_MAX + 1];
char *lang; char *lang;
} docs_t; } docs_t;
bool docs_init(docs_t *docs, char *dir); bool docs_init(docs_t *docs, char *dir);

View File

@ -1,10 +0,0 @@
#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);

View File

@ -4,8 +4,16 @@
#include <stdint.h> #include <stdint.h>
#include <ctype.h> #include <ctype.h>
#define util_toupper(str) \ typedef struct {
for (char *c = str; *c != 0; c++) \ char *content;
uint64_t size;
} util_file_t;
#define util_toupper(str) \
for (char *c = str; *c != 0; c++) \
*c = toupper(*c) *c = toupper(*c)
uint64_t util_endswith(char *str, char *suf); uint64_t util_endswith(char *str, char *suf);
void util_send(ctorm_res_t *res, uint16_t code, cJSON *json); void util_send(ctorm_res_t *res, uint16_t code, cJSON *json);
util_file_t *util_file_load(int dirfd, char *path);
void util_file_free(util_file_t *file);
bool util_parse_doc_name(char *name, char **lang, const char *ext);

View File

@ -1,4 +0,0 @@
{
"title": "privacy",
"desc": "Privacy policy"
}

View File

@ -1,4 +0,0 @@
{
"title": "gizlilik",
"desc": "Gizlilik ilkesi"
}

View File

@ -10,26 +10,22 @@
#include "util.h" #include "util.h"
option_t options[] = { option_t options[] = {
// name value requied {"host", "0.0.0.0:7003", true }, // host the server should listen on
{"host", "0.0.0.0:7003", true }, // host the server should listen on {"docs_dir", "./docs", true }, // documentation directory
{"dir", "./pages", true }, // documentation pages directory {"", NULL, false},
{"", NULL, false},
}; };
bool config_load(config_t *conf) { bool config_load(config_t *conf) {
memset(conf, 0, sizeof(*conf)); bzero(conf, sizeof(*conf));
char name_env[OPT_NAME_MAX + 10], name_copy[OPT_NAME_MAX], *value = NULL; char name_env[OPT_NAME_MAX + 10], name_copy[OPT_NAME_MAX], *value = NULL;
conf->options = options; conf->options = options;
for (option_t *opt = conf->options; opt->value != NULL; for (option_t *opt = conf->options; opt->value != NULL; opt++, conf->count++) {
opt++, conf->count++) { strcpy(name_copy, opt->name);
// convert option name to environment variable name
strncpy(name_copy, opt->name, OPT_NAME_MAX);
util_toupper(name_copy); util_toupper(name_copy);
snprintf(name_env, sizeof(name_env), "WEBSITE_%s", name_copy); snprintf(name_env, sizeof(name_env), "WEBSITE_%s", name_copy);
// attempt to load the value from the environment
if ((value = getenv(name_env)) != NULL) if ((value = getenv(name_env)) != NULL)
opt->value = value; opt->value = value;
@ -39,9 +35,7 @@ bool config_load(config_t *conf) {
if (!opt->required || NULL != opt->value) if (!opt->required || NULL != opt->value)
continue; continue;
ctorm_fail("please specify a value for the required config option: %s (%s)", ctorm_fail("please specify a value for the required config option: %s (%s)", opt->name, name_env);
opt->name,
name_env);
errno = EFAULT; errno = EFAULT;
return false; return false;
} }

View File

@ -1,6 +1,6 @@
#include <linux/limits.h> #include <linux/limits.h>
#include <dirent.h> #include <dirent.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdint.h> #include <stdint.h>
@ -9,7 +9,7 @@
#define DOCS_LANG_CODE_LEN 2 #define DOCS_LANG_CODE_LEN 2
bool _docs_parse_name(docs_t *docs, char *ext) { bool __docs_parse_name(docs_t *docs, char *ext) {
// check the extension // check the extension
uint64_t ext_pos = util_endswith(docs->name, ext); uint64_t ext_pos = util_endswith(docs->name, ext);
@ -22,8 +22,7 @@ bool _docs_parse_name(docs_t *docs, char *ext) {
// example.en\0json\0 // example.en\0json\0
// | // |
// `--- find this // `--- find this
for (docs->lang = docs->name; *docs->lang != 0 && *docs->lang != '.'; for (docs->lang = docs->name; *docs->lang != 0 && *docs->lang != '.'; docs->lang++)
docs->lang++)
continue; continue;
if (*docs->lang != '.') if (*docs->lang != '.')
@ -40,11 +39,11 @@ bool _docs_parse_name(docs_t *docs, char *ext) {
return strlen(docs->lang) == DOCS_LANG_CODE_LEN && *docs->name != 0; return strlen(docs->lang) == DOCS_LANG_CODE_LEN && *docs->name != 0;
} }
void _docs_clean(docs_t *docs) { void __docs_clean(docs_t *docs) {
if (NULL == docs->file) if (NULL == docs->file)
return; return;
file_free(docs->file); util_file_free(docs->file);
docs->file = NULL; docs->file = NULL;
} }
@ -54,7 +53,7 @@ bool docs_init(docs_t *docs, char *dir) {
return false; return false;
} }
memset(docs, 0, sizeof(*docs)); bzero(docs, sizeof(*docs));
return NULL != (docs->dir = opendir(dir)); return NULL != (docs->dir = opendir(dir));
} }
@ -65,15 +64,15 @@ char *docs_next(docs_t *docs, char *name, bool content) {
} }
struct dirent *ent = NULL; struct dirent *ent = NULL;
_docs_clean(docs); __docs_clean(docs);
while (NULL != (ent = readdir(docs->dir))) { while (NULL != (ent = readdir(docs->dir))) {
if (*ent->d_name == '.') if (*ent->d_name == '.')
continue; continue;
strncpy(docs->name, ent->d_name, NAME_MAX); strcpy(docs->name, ent->d_name);
if (!_docs_parse_name(docs, content ? ".md" : ".json")) if (!__docs_parse_name(docs, content ? ".md" : ".json"))
continue; continue;
if (NULL == name || strncmp(docs->name, name, NAME_MAX) == 0) if (NULL == name || strncmp(docs->name, name, NAME_MAX) == 0)
@ -85,7 +84,7 @@ char *docs_next(docs_t *docs, char *name, bool content) {
return NULL; return NULL;
} }
if (NULL == (docs->file = file_load(dirfd(docs->dir), ent->d_name))) if (NULL == (docs->file = util_file_load(dirfd(docs->dir), ent->d_name)))
return NULL; return NULL;
return docs->file->content; return docs->file->content;
@ -100,8 +99,8 @@ void docs_free(docs_t *docs) {
if (NULL == docs) if (NULL == docs)
return; return;
_docs_clean(docs); __docs_clean(docs);
closedir(docs->dir); closedir(docs->dir);
memset(docs, 0, sizeof(*docs)); bzero(docs, sizeof(*docs));
} }

View File

@ -1,56 +0,0 @@
#include <sys/mman.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include "file.h"
file_t *file_load(int dirfd, char *path) {
if (NULL == path) {
errno = EINVAL;
return NULL;
}
file_t *file = NULL;
int fd = -1;
// open the taarget file
if ((fd = openat(dirfd, path, O_RDONLY)) < 0)
goto end; // errno set by open
// allocate a new file structure
if (NULL == (file = calloc(1, sizeof(file_t))))
goto end; // errno set by malloc
// calculate the file size
if ((file->size = lseek(fd, 0, SEEK_END)) < 0)
goto end;
// memory map the file
if (NULL ==
(file->content = mmap(0, file->size, PROT_READ, MAP_PRIVATE, fd, 0)))
goto end; // errno set by mmap
end:
if (fd != -1)
close(fd);
if (NULL != file && NULL == file->content) {
free(file);
return NULL;
}
return file;
}
void file_free(file_t *file) {
if (NULL == file)
return;
munmap(file->content, file->size);
free(file);
}

View File

@ -1,4 +1,3 @@
#include <ctorm/app.h>
#include <ctorm/ctorm.h> #include <ctorm/ctorm.h>
#include <stdlib.h> #include <stdlib.h>
@ -14,34 +13,28 @@ int main() {
if (!config_load(&conf)) if (!config_load(&conf))
return EXIT_FAILURE; return EXIT_FAILURE;
host = config_get(&conf, "host");
if (NULL == (host = config_get(&conf, "host"))) {
ctorm_fail("failed to get the host configuration");
return EXIT_FAILURE;
}
// initialize the app config
ctorm_config_new(&app_config); ctorm_config_new(&app_config);
app_config.disable_logging = true; app_config.disable_logging = true;
app = ctorm_app_new(&app_config);
// create a new app
app = ctorm_app_new(&app_config);
// middlewares // middlewares
ALL(app, "/*", route_cors); MIDDLEWARE_ALL(app, "/*", route_cors);
ALL(app, "/*/*", route_cors); MIDDLEWARE_ALL(app, "/*/*", route_cors);
MIDDLEWARE_ALL(app, "/*/*/*", route_cors);
// routes // routes
GET(app, "/list", route_list); GET(app, "/list", route_list);
GET(app, "/get/:name", route_get); GET(app, "/get/:name", route_get);
ctorm_app_default(app, route_notfound); ctorm_app_all(app, route_notfound);
ctorm_app_local(app, "config", &conf); ctorm_app_local(app, "config", &conf);
ctorm_info("starting the web server on %s", host); ctorm_info("starting the web server on %s", host);
if (!ctorm_app_run(app, host)) if (!ctorm_app_run(app, host))
ctorm_fail("failed to start the app: %s", ctorm_error()); ctorm_fail("failed to start the app: %s", ctorm_geterror());
ctorm_app_free(app); ctorm_app_free(app);
return EXIT_SUCCESS; return EXIT_SUCCESS;

View File

@ -3,8 +3,7 @@
void route_cors(ctorm_req_t *req, ctorm_res_t *res) { void route_cors(ctorm_req_t *req, ctorm_res_t *res) {
RES_SET("Access-Control-Allow-Origin", "*"); RES_SET("Access-Control-Allow-Origin", "*");
RES_SET("Access-Control-Allow-Headers", RES_SET("Access-Control-Allow-Headers",
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, " "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, "
"Authorization, accept, origin, Cache-Control, "
"X-Requested-With"); "X-Requested-With");
RES_SET("Access-Control-Allow-Methods", "PUT, DELETE, GET"); RES_SET("Access-Control-Allow-Methods", "PUT, DELETE, GET");
} }

View File

@ -1,30 +1,31 @@
#include <linux/limits.h>
#include <cjson/cJSON.h> #include <cjson/cJSON.h>
#include <dirent.h>
#include <linux/limits.h>
#include <ctorm/ctorm.h> #include <ctorm/ctorm.h>
#include <dirent.h> #include <string.h>
#include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include "routes.h" #include "routes.h"
#include "config.h" #include "config.h"
#include "util.h"
#include "docs.h" #include "docs.h"
void route_get(ctorm_req_t *req, ctorm_res_t *res) { void route_get(ctorm_req_t *req, ctorm_res_t *res) {
config_t *conf = REQ_LOCAL("config"); config_t *conf = REQ_LOCAL("config");
char *name = REQ_PARAM("name"); char *doc_name = REQ_PARAM("name");
char *dir = config_get(conf, "dir"), *doc_data = NULL; char *docs_dir = config_get(conf, "docs_dir"), *doc_data = NULL;
cJSON *json = NULL, *doc_json = NULL; cJSON *json = NULL, *doc_json = NULL;
docs_t docs; docs_t docs;
if (NULL == name) { if (NULL == doc_name) {
ctorm_fail("documentation name not specified (how did that even happend)"); ctorm_fail("documentation name not specified (how did that even happend)");
util_send(res, 500, NULL); util_send(res, 500, NULL);
goto end; goto end;
} }
if (!docs_init(&docs, dir)) { if (!docs_init(&docs, docs_dir)) {
ctorm_fail("docs_init failed: %s", ctorm_error()); ctorm_fail("docs_init failed: %s", ctorm_geterror());
util_send(res, 500, NULL); util_send(res, 500, NULL);
goto end; goto end;
} }
@ -35,7 +36,7 @@ void route_get(ctorm_req_t *req, ctorm_res_t *res) {
goto end; goto end;
} }
while (NULL != (doc_data = docs_next(&docs, name, false))) { while (NULL != (doc_data = docs_next(&docs, doc_name, false))) {
if (NULL == (doc_json = cJSON_Parse(doc_data))) { if (NULL == (doc_json = cJSON_Parse(doc_data))) {
ctorm_fail("failed to parse JSON: %s (%s)", docs.name, docs.lang); ctorm_fail("failed to parse JSON: %s (%s)", docs.name, docs.lang);
continue; continue;
@ -52,7 +53,7 @@ void route_get(ctorm_req_t *req, ctorm_res_t *res) {
docs_reset(&docs); docs_reset(&docs);
while (NULL != (doc_data = docs_next(&docs, name, true))) { while (NULL != (doc_data = docs_next(&docs, doc_name, true))) {
if (NULL == (doc_json = cJSON_GetObjectItem(json, docs.lang))) if (NULL == (doc_json = cJSON_GetObjectItem(json, docs.lang)))
continue; continue;

View File

@ -3,21 +3,21 @@
#include <ctorm/ctorm.h> #include <ctorm/ctorm.h>
#include <dirent.h> #include <dirent.h>
#include <string.h>
#include <stdio.h> #include <stdio.h>
#include "routes.h" #include "routes.h"
#include "config.h" #include "config.h"
#include "util.h"
#include "docs.h" #include "docs.h"
void route_list(ctorm_req_t *req, ctorm_res_t *res) { void route_list(ctorm_req_t *req, ctorm_res_t *res) {
config_t *conf = REQ_LOCAL("config"); config_t *conf = REQ_LOCAL("config");
char *dir = config_get(conf, "dir"), *doc_data = NULL; char *docs_dir = config_get(conf, "docs_dir"), *doc_data = NULL;
cJSON *array = NULL, *json = NULL, *doc_json = NULL; cJSON *array = NULL, *json = NULL, *doc_json = NULL;
docs_t docs; docs_t docs;
if (!docs_init(&docs, dir)) { if (!docs_init(&docs, docs_dir)) {
ctorm_fail("docs_init failed: %s", ctorm_error()); ctorm_fail("docs_init failed: %s", ctorm_geterror());
util_send(res, 500, NULL); util_send(res, 500, NULL);
goto end; goto end;
} }
@ -31,8 +31,7 @@ void route_list(ctorm_req_t *req, ctorm_res_t *res) {
while (NULL != (doc_data = docs_next(&docs, NULL, false))) { while (NULL != (doc_data = docs_next(&docs, NULL, false))) {
if (NULL == (array = cJSON_GetObjectItem(json, docs.lang)) && if (NULL == (array = cJSON_GetObjectItem(json, docs.lang)) &&
NULL == (array = cJSON_AddArrayToObject(json, docs.lang))) { NULL == (array = cJSON_AddArrayToObject(json, docs.lang))) {
ctorm_fail( ctorm_fail("failed to create an array object for the language %s", docs.lang);
"failed to create an array object for the language %s", docs.lang);
continue; continue;
} }

View File

@ -1,8 +1,17 @@
#include <linux/limits.h> #include <linux/limits.h>
#include <ctorm/ctorm.h> #include <ctorm/ctorm.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <stdint.h> #include <stdint.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <errno.h> #include <errno.h>
#include "util.h" #include "util.h"
@ -48,3 +57,45 @@ void util_send(ctorm_res_t *res, uint16_t code, cJSON *json) {
RES_JSON(json); RES_JSON(json);
} }
util_file_t *util_file_load(int dirfd, char *path) {
if (NULL == path) {
errno = EINVAL;
return NULL;
}
util_file_t *file = NULL;
struct stat buf;
int fd = -1;
if (NULL == (file = malloc(sizeof(util_file_t))))
goto end; // errno set by malloc
if ((fd = openat(dirfd, path, O_RDONLY)) < 0)
goto end; // errno set by open
if (fstat(fd, &buf) < 0)
goto end; // errno set by fstat
if (NULL == (file->content = mmap(0, (file->size = buf.st_size), PROT_READ, MAP_PRIVATE, fd, 0)))
goto end; // errno set by mmap
end:
if (fd != -1)
close(fd);
if (NULL == file->content) {
free(file);
return NULL;
}
return file;
}
void util_file_free(util_file_t *file) {
if (NULL == file)
return;
munmap(file->content, file->size);
free(file);
}