commit
95986d170d
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,2 +1,7 @@
|
|||||||
|
data.db
|
||||||
*.yaml
|
*.yaml
|
||||||
|
*.yml
|
||||||
*.env
|
*.env
|
||||||
|
|
||||||
|
# don't ignore example deployment stuff
|
||||||
|
!deploy/*
|
||||||
|
13
Makefile
Normal file
13
Makefile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
SERVERS = app api doc
|
||||||
|
|
||||||
|
all: $(SERVERS)
|
||||||
|
for server in $^ ; do \
|
||||||
|
make -C $$server ; \
|
||||||
|
done
|
||||||
|
|
||||||
|
format:
|
||||||
|
for server in $(SERVERS) ; do \
|
||||||
|
make -C $$server format ; \
|
||||||
|
done
|
||||||
|
|
||||||
|
.PHONY: format
|
102
README.md
102
README.md
@ -14,80 +14,72 @@ and 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
|
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
|
framework which offers an [Express](https://expressjs.com/) like experience. I choose Fiber since I've used
|
||||||
[Gin](https://github.com/gin-gonic/gin) (see history section).
|
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
|
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).
|
I'm using [mattn's sqlite3 driver](https://github.com/mattn/go-sqlite3).
|
||||||
|
|
||||||
|
### `doc`
|
||||||
|
Contains the documentation server, written in C. It uses the [ctorm](https://github.com/ngn13/ctorm) web
|
||||||
|
framework, which is a framework that I myself wrote. Unlike the frontend application or the API server, it's not
|
||||||
|
accessable by public, the frontend application gets the documentation content from this server and renders it using
|
||||||
|
SSR. The reason I don't use the API for hosting the documentation content is that I want a separate server for hosting
|
||||||
|
static content, API is only for hosting dynamic stuff.
|
||||||
|
|
||||||
### `admin`
|
### `admin`
|
||||||
The frontend application does not contain an admin interface, I do the administration stuff (such as
|
The frontend application does not contain an admin interface, I do the administration stuff (such as adding news posts,
|
||||||
adding posts, adding services etc.) using the python script in this directory. This script can be
|
adding services etc.) using the python script in this directory. This script can be installed on to the PATH by running
|
||||||
installed on to the PATH by running the `install.sh` script. After installation it can be used
|
the Makefile install script. After installation it can be used by running `admin_script`.
|
||||||
by running `admin_script`.
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
Easiest way to deploy is to use docker. I have created a `compose.yml` file so the API and the
|
Easiest way to deploy is to use docker. There is `compose.yml` and a `run.sh` script in the [deploy](deploy/) directory
|
||||||
frontend application can be deployed easily with just the `docker-compose up` command:
|
that can be used to startup all the docker containers. Configuration options are passed during build time for the frontend
|
||||||
```yaml
|
application, and for others it's passed with environment variables.
|
||||||
version: "3"
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: ./app
|
|
||||||
args:
|
|
||||||
API_URL: https://api.ngn.tf
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:7002:3000"
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
|
|
||||||
api:
|
|
||||||
build:
|
|
||||||
context: ./api
|
|
||||||
environment:
|
|
||||||
- API_PASSWORD="securepassword"
|
|
||||||
- API_FRONTEND_URL="https://ngn.tf"
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:7001:7001"
|
|
||||||
volumes:
|
|
||||||
- ./api/data.db:/app/data.db
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 just a simple HTML/CSS page,
|
- **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was just a simple HTML/CSS page,
|
||||||
I never published any of the source code and I wiped the local copy on my USB drive in early 2022
|
I never published any of the source code and I wiped the local copy on my USB drive in early 2022, I still
|
||||||
|
remember what it looked like though, it looked like I made entire website in microsoft paint... while blindfoled,
|
||||||
|
so yeah it was shit.
|
||||||
|
|
||||||
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code
|
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code
|
||||||
was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png)
|
was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png).
|
||||||
|
|
||||||
- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework)
|
- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework) my website with one
|
||||||
my website with one of the fancy JS frameworks. I decided to go with Svelte. Not the kit version,
|
of the fancy JS frameworks. I decided to go with Svelte. Not the kit version, at the time svelte did not support SSR.
|
||||||
at the time svelte did not support SSR. I do not remember writting an API for it so I guess I just
|
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
|
||||||
updated it everytime I wanted to add content? It was pretty much like a static website and was hosted
|
much like a static website and was hosted on `ngn13.fun` as at this point I had my own hosting. The source code for
|
||||||
on `ngn13.fun` as at this point I had my own hosting. The source code for this website was in a
|
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
|
||||||
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 :(
|
to find it. I also do not remember how it looked like, sooo this version is pretty much lost :(
|
||||||
|
|
||||||
- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework
|
- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework my website one more
|
||||||
my website one more time in NuxtJS. I had really "fun time" using vue stuff. As NuxtJS supported
|
time in NuxtJS. I had really "fun" time using vue stuff. As NuxtJS supported server-side code, this website had its own
|
||||||
server-side code, this website had its own built in API. This website was also hosted on `ngn13.fun`
|
built in API. This website was also hosted on `ngn13.fun`. This also the first version that lives on this git repository.
|
||||||
|
|
||||||
- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite and rework my website
|
- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite and rework my website again, this time
|
||||||
again, this time I was going with SvelteKit as I haven't had the greatest experience with NuxtJS.
|
I was going with SvelteKit as I haven't had the greatest experience with NuxtJS. SvelteKit was really fun to work with
|
||||||
SvelteKit was really fun to work with and I got my new website done pretty quickly. (I don't wanna
|
and I got my new website done pretty quickly. (I don't wanna brag or something but I really imporeved the CSS/styling
|
||||||
brag or something but I really imporeved the CSS/styling stuff ya know). I also wrote a new API
|
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
|
||||||
with Go and Gin. I did not publish the source code for the API, its still on my local gitea
|
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.
|
||||||
server tho. This website was hosted on `ngn13.fun` as well
|
|
||||||
|
|
||||||
- **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0,
|
- **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0, the big changes are in
|
||||||
the big changes are in the API. I rewrote the API with Fiber. This version was the first version which is hosted on
|
the API. I rewrote the API with Fiber. This version was the first version hosted on `ngn.tf` which is my new domain name.
|
||||||
`ngn.tf` which is my new domain name btw
|
|
||||||
|
|
||||||
- **v5.0 (2024 october - ...)**: The current major version of my website, has small UI and API tweaks when
|
- **v5.0 (2024 october - 2025 january)**: This version just had simple frontend UI changes compared to 4.0, at this
|
||||||
compared to 4.0
|
point I was thinking about doing a massive rework (which I did with 6.0), however I was working on some other shit at
|
||||||
|
the time, so I just did some small changes with the limited time I had for this project.
|
||||||
|
|
||||||
|
- **v6.0 (2025 january - ...)**: The current major version of my website, frontend had a massive rework, API has been
|
||||||
|
cleaned up and extended to do status checking for the services I host. The `doc` server has been added to the mix
|
||||||
|
so I can host static documentation. The most important thing about this version is that it adds multi-language support,
|
||||||
|
so literally everything on the website (including the API and documentation content) is localized for both English
|
||||||
|
and Turkish, which was something I wanted to do for the longest time ever.
|
||||||
|
|
||||||
|
Damn it has been 4 years since I wrote that shit HTML page huh? Time flies...
|
||||||
|
|
||||||
## Screenshots (from v4.0)
|
## Screenshots (from v4.0)
|
||||||

|

|
||||||
|
12
admin/Makefile
Normal file
12
admin/Makefile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
SRCS = $(wildcard *.py)
|
||||||
|
PREFIX = /usr
|
||||||
|
|
||||||
|
all:
|
||||||
|
|
||||||
|
format:
|
||||||
|
black $(SRCS)
|
||||||
|
|
||||||
|
install:
|
||||||
|
install -Dm755 admin.py $(PREFIX)/bin/admin_script
|
||||||
|
|
||||||
|
.PHONY: format install
|
500
admin/admin.py
500
admin/admin.py
@ -1,176 +1,404 @@
|
|||||||
#!/bin/python3
|
#!/bin/python3
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Administration script for my website (ngn.tf)
|
|
||||||
#############################################
|
website/admin | Administration script for my personal website
|
||||||
I really enjoy doing stuff from the terminal,
|
written by ngn (https://ngn.tf) (2025)
|
||||||
so I wrote this simple python script that interacts
|
|
||||||
with the API and lets me add/remove new posts/services
|
This program is free software: you can redistribute it and/or modify
|
||||||
from the terminal
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from os import remove, getenv
|
from urllib.parse import quote_plus
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
from datetime import datetime, UTC
|
||||||
|
from colorama import Fore, Style
|
||||||
|
from json import dumps, loads
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
import requests as req
|
import requests as req
|
||||||
|
from os import getenv
|
||||||
from sys import argv
|
from sys import argv
|
||||||
|
|
||||||
URL = ""
|
|
||||||
|
|
||||||
def join(pth: str) -> str:
|
# logger used by the script
|
||||||
if URL == None:
|
class Log:
|
||||||
return ""
|
def __init__(self) -> None:
|
||||||
|
self.reset = Fore.RESET + Style.RESET_ALL
|
||||||
|
|
||||||
if URL.endswith("/"):
|
def info(self, m: str) -> None:
|
||||||
return URL+pth
|
print(Fore.BLUE + Style.BRIGHT + "[*]" + self.reset + " " + m)
|
||||||
return URL+"/"+pth
|
|
||||||
|
|
||||||
def get_token() -> str:
|
def error(self, m: str) -> None:
|
||||||
try:
|
print(Fore.RED + Style.BRIGHT + "[-]" + self.reset + " " + m)
|
||||||
f = open("/tmp/wa", "r")
|
|
||||||
token = f.read()
|
|
||||||
f.close()
|
|
||||||
return token
|
|
||||||
except:
|
|
||||||
print("[-] You are not authenticated")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
def login() -> None:
|
def input(self, m: str) -> str:
|
||||||
pwd = getpass("[>] Enter your password: ")
|
return input(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
|
||||||
res = req.get(join("admin/login")+f"?pass={pwd}").json()
|
|
||||||
if res["error"] != "":
|
|
||||||
print(f"[-] Error logging in: {res['error']}")
|
|
||||||
return
|
|
||||||
|
|
||||||
token = res["token"]
|
def password(self, m: str) -> str:
|
||||||
f = open("/tmp/wa", "w")
|
return getpass(Fore.CYAN + Style.BRIGHT + "[?]" + self.reset + " " + m + ": ")
|
||||||
f.write(token)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def logout() -> None:
|
|
||||||
token = get_token()
|
|
||||||
res = req.get(join("admin/logout"), headers={
|
|
||||||
"Authorization": token
|
|
||||||
}).json()
|
|
||||||
if res["error"] != "":
|
|
||||||
print(f"[-] Error logging out: {res['error']}")
|
|
||||||
return
|
|
||||||
|
|
||||||
remove("/tmp/wa")
|
# API interface for the admin endpoints
|
||||||
print("[+] Logged out")
|
class AdminAPI:
|
||||||
|
def __init__(self, url: str, password: str) -> None:
|
||||||
|
self.languages: List[str] = [
|
||||||
|
"en",
|
||||||
|
"tr",
|
||||||
|
] # languages supported by multilang fields
|
||||||
|
self.password = password
|
||||||
|
self.api_url = url
|
||||||
|
|
||||||
def add_post() -> None:
|
def _title_to_id(self, title: str) -> str:
|
||||||
token = get_token()
|
return title.lower().replace(" ", "_")
|
||||||
title = input("[>] Post title: ")
|
|
||||||
author = input("[>] Post author: ")
|
|
||||||
content_file = input("[>] Post content file: ")
|
|
||||||
public = input("[>] Should post be public? (y/n): ")
|
|
||||||
|
|
||||||
try:
|
def _check_multilang_field(self, ml: Dict[str, str]) -> bool:
|
||||||
f = open(content_file, "r")
|
for lang in self.languages:
|
||||||
content = f.read()
|
if lang in ml and ml[lang] != "":
|
||||||
f.close()
|
return True
|
||||||
except:
|
return False
|
||||||
print("[-] Content file not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
res = req.put(join("admin/blog/add"), json={
|
def _api_url_join(self, path: str) -> str:
|
||||||
"title": title,
|
api_has_slash = self.api_url.endswith("/")
|
||||||
"author": author,
|
path_has_slash = path.startswith("/")
|
||||||
"content": content,
|
|
||||||
"public": 1 if public == "y" else 0
|
|
||||||
}, headers={
|
|
||||||
"Authorization": token
|
|
||||||
}).json()
|
|
||||||
|
|
||||||
if res["error"] != "":
|
if api_has_slash or path_has_slash:
|
||||||
print(f"[-] Error adding post: {res['error']}")
|
return self.api_url + path
|
||||||
return
|
elif api_has_slash and path_has_slash:
|
||||||
|
return self.api_url + path[1:]
|
||||||
|
else:
|
||||||
|
return self.api_url + "/" + path
|
||||||
|
|
||||||
print("[+] Post has been added")
|
def _to_json(self, res: req.Response) -> dict:
|
||||||
|
if res.status_code == 403:
|
||||||
|
raise Exception("Authentication failed")
|
||||||
|
|
||||||
def remove_post() -> None:
|
json = res.json()
|
||||||
token = get_token()
|
|
||||||
id = input("[>] Post ID: ")
|
|
||||||
res = req.delete(join("admin/blog/remove")+f"?id={id}", headers={
|
|
||||||
"Authorization": token
|
|
||||||
}).json()
|
|
||||||
|
|
||||||
if res["error"] != "":
|
if json["error"] != "":
|
||||||
print(f"[-] Error removing post: {res['error']}")
|
raise Exception("API error: %s" % json["error"])
|
||||||
return
|
|
||||||
|
|
||||||
print("[-] Post has been removed")
|
return json
|
||||||
|
|
||||||
def add_service() -> None:
|
def PUT(self, url: str, data: dict) -> req.Response:
|
||||||
token = get_token()
|
return self._to_json(
|
||||||
name = input("[>] Serivce name: ")
|
req.put(
|
||||||
desc = input("[>] Serivce desc: ")
|
self._api_url_join(url),
|
||||||
link = input("[>] Serivce URL: ")
|
json=data,
|
||||||
|
headers={"Authorization": self.password},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
res = req.put(join("admin/service/add"), json={
|
def DELETE(self, url: str) -> req.Response:
|
||||||
"name": name,
|
return self._to_json(
|
||||||
"desc": desc,
|
req.delete(
|
||||||
"url": link
|
self._api_url_join(url), headers={"Authorization": self.password}
|
||||||
}, headers={
|
)
|
||||||
"Authorization": token
|
)
|
||||||
}).json()
|
|
||||||
|
|
||||||
if res["error"] != "":
|
def GET(self, url: str) -> req.Response:
|
||||||
print(f"[-] Error adding service: {res['error']}")
|
return self._to_json(
|
||||||
return
|
req.get(self._api_url_join(url), headers={"Authorization": self.password})
|
||||||
|
)
|
||||||
|
|
||||||
print("[+] Service has been added")
|
def add_service(self, service: Dict[str, str]):
|
||||||
|
if "name" not in service or service["name"] == "":
|
||||||
|
raise Exception('Service structure is missing required "name" field')
|
||||||
|
|
||||||
def remove_service() -> None:
|
if "desc" not in service:
|
||||||
token = get_token()
|
raise Exception('Service structure is missing required "desc" field')
|
||||||
name = input("[>] Service name: ")
|
|
||||||
res = req.delete(join("admin/service/remove")+f"?name={name}", headers={
|
|
||||||
"Authorization": token
|
|
||||||
}).json()
|
|
||||||
|
|
||||||
if res["error"] != "":
|
if (
|
||||||
print(f"[-] Error removing service: {res['error']}")
|
("clear" not in service or service["clear"] == "")
|
||||||
return
|
and ("onion" not in service or service["onion"] == "")
|
||||||
|
and ("i2p" not in service or service["i2p"] == "")
|
||||||
|
):
|
||||||
|
raise Exception(
|
||||||
|
'Service structure is missing "clear", "onion" '
|
||||||
|
+ 'and "i2p" field, at least one needed'
|
||||||
|
)
|
||||||
|
|
||||||
print("[+] Serivce has been removed")
|
if not self._check_multilang_field(service["desc"]):
|
||||||
|
raise Exception(
|
||||||
|
'Service structure field "desc" needs at least '
|
||||||
|
+ "one supported language entry"
|
||||||
|
)
|
||||||
|
|
||||||
cmds = {
|
self.PUT("/v1/admin/service/add", service)
|
||||||
"login": login,
|
|
||||||
"logout": logout,
|
|
||||||
"add_post": add_post,
|
|
||||||
"remove_post": remove_post,
|
|
||||||
"add_service": add_service,
|
|
||||||
"remove_service": remove_service,
|
|
||||||
}
|
|
||||||
|
|
||||||
def main():
|
def del_service(self, name: str) -> None:
|
||||||
global URL
|
if name == "":
|
||||||
URL = getenv("API")
|
raise Exception("Service name cannot be empty")
|
||||||
if URL == None or URL == "":
|
|
||||||
print("[-] API enviroment variable not set")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if len(argv) != 2:
|
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(name))
|
||||||
print(f"[-] Usage: admin_script <command>")
|
|
||||||
print(f"[+] Run \"admin_script help\" to get all commands")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if argv[1] == "help":
|
def add_project(self, project: Dict[str, str]):
|
||||||
print("Avaliable commands:")
|
if "name" not in project or project["name"] == "":
|
||||||
for k in cmds.keys():
|
raise Exception('Project structure is missing required "name" field')
|
||||||
print(f" {k}")
|
|
||||||
exit()
|
if "desc" not in project:
|
||||||
|
raise Exception('Project structure is missing required "desc" field')
|
||||||
|
|
||||||
|
if not self._check_multilang_field(project["desc"]):
|
||||||
|
raise Exception(
|
||||||
|
'Project structure field "desc" needs at least '
|
||||||
|
+ "one supported language entry"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.PUT("/v1/admin/project/add", project)
|
||||||
|
|
||||||
|
def del_project(self, name: str) -> None:
|
||||||
|
if name == "":
|
||||||
|
raise Exception("Project name cannot be empty")
|
||||||
|
|
||||||
|
self.DELETE("/v1/admin/project/del?name=%s" % quote_plus(name))
|
||||||
|
|
||||||
|
def check_services(self) -> None:
|
||||||
|
self.GET("/v1/admin/service/check")
|
||||||
|
|
||||||
|
def add_news(self, news: Dict[str, str]):
|
||||||
|
if "id" not in news or news["id"] == "":
|
||||||
|
raise Exception('News structure is missing required "id" field')
|
||||||
|
|
||||||
|
if "author" not in news or news["author"] == "":
|
||||||
|
raise Exception('News structure is missing required "author" field')
|
||||||
|
|
||||||
|
if "title" not in news:
|
||||||
|
raise Exception('News structure is missing required "title" field')
|
||||||
|
|
||||||
|
if "content" not in news:
|
||||||
|
raise Exception('News structure is missing required "content" field')
|
||||||
|
|
||||||
|
if not self._check_multilang_field(news["title"]):
|
||||||
|
raise Exception(
|
||||||
|
'News structure field "title" needs at least '
|
||||||
|
+ "one supported language entry"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self._check_multilang_field(news["content"]):
|
||||||
|
raise Exception(
|
||||||
|
'News structure field "content" needs at least '
|
||||||
|
+ "one supported language entry"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.PUT("/v1/admin/news/add", news)
|
||||||
|
|
||||||
|
def del_news(self, id: str) -> None:
|
||||||
|
if id == "":
|
||||||
|
raise Exception("News ID cannot be empty")
|
||||||
|
|
||||||
|
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(id))
|
||||||
|
|
||||||
|
def logs(self) -> List[Dict[str, Any]]:
|
||||||
|
return self.GET("/v1/admin/logs")
|
||||||
|
|
||||||
|
|
||||||
|
class AdminScript:
|
||||||
|
def __init__(self):
|
||||||
|
self.log: Log = Log()
|
||||||
|
self.api: AdminAPI = None
|
||||||
|
self.commands = {
|
||||||
|
"add_service": self.add_service,
|
||||||
|
"del_service": self.del_service,
|
||||||
|
"add_project": self.add_project,
|
||||||
|
"del_project": self.del_project,
|
||||||
|
"add_news": self.add_news,
|
||||||
|
"del_news": self.del_news,
|
||||||
|
"check_services": self.check_services,
|
||||||
|
"logs": self.get_logs,
|
||||||
|
}
|
||||||
|
self.api_url_env = "API_URL"
|
||||||
|
|
||||||
|
def __format_time(self, ts: int) -> str:
|
||||||
|
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
|
||||||
|
|
||||||
|
def __load_json_file(self, file: str) -> Dict[str, Any]:
|
||||||
|
with open(file, "r") as f:
|
||||||
|
data = loads(f.read())
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __dump_json_file(self, data: Dict[str, Any], file: str) -> None:
|
||||||
|
with open(file, "w") as f:
|
||||||
|
data = dumps(data, indent=2)
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
def run(self) -> bool:
|
||||||
|
if len(argv) < 2 or len(argv) > 3:
|
||||||
|
self.log.error("Usage: %s [command] <file>" % argv[0])
|
||||||
|
self.log.info("Here is a list of available commands:")
|
||||||
|
|
||||||
|
for command in self.commands.keys():
|
||||||
|
print("\t%s" % command)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
url = getenv(self.api_url_env)
|
||||||
|
valid_cmd = False
|
||||||
|
|
||||||
|
if url is None:
|
||||||
|
self.log.error(
|
||||||
|
"Please specify the API URL using %s environment variable"
|
||||||
|
% self.api_url_env
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
for cmd in self.commands:
|
||||||
|
if argv[1] == cmd:
|
||||||
|
valid_cmd = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not valid_cmd:
|
||||||
|
self.log.error(
|
||||||
|
"Invalid command, run the script with no commands to list the available commands"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
for k in cmds.keys():
|
|
||||||
if k != argv[1]:
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
cmds[k]()
|
password = self.log.password("Please enter the admin password")
|
||||||
except KeyboardInterrupt:
|
self.api = AdminAPI(url, password)
|
||||||
pass
|
|
||||||
exit()
|
if len(argv) == 2:
|
||||||
|
self.handle_command(argv[1])
|
||||||
|
|
||||||
|
elif len(argv) == 3:
|
||||||
|
self.handle_command(argv[1], argv[2])
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.log.error("Command cancelled")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error("Command failed: %s" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# service commands
|
||||||
|
def add_service(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
|
data: Dict[str, str] = {}
|
||||||
|
data["desc"] = {}
|
||||||
|
|
||||||
|
data["name"] = self.log.input("Serivce name")
|
||||||
|
|
||||||
|
for lang in self.api.languages:
|
||||||
|
data["desc"][lang] = self.log.input("Serivce desc (%s)" % lang)
|
||||||
|
|
||||||
|
data["check_url"] = self.log.input("Serivce status check URL")
|
||||||
|
data["clear"] = self.log.input("Serivce clearnet URL")
|
||||||
|
data["onion"] = self.log.input("Serivce onion URL")
|
||||||
|
data["i2p"] = self.log.input("Serivce I2P URL")
|
||||||
|
|
||||||
|
self.api.add_service(data)
|
||||||
|
self.log.info("Service has been added")
|
||||||
|
|
||||||
|
def del_service(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
|
data: Dict[str, str] = {}
|
||||||
|
data["name"] = self.log.input("Service name")
|
||||||
|
|
||||||
|
self.api.del_service(data["name"])
|
||||||
|
self.log.info("Service has been deleted")
|
||||||
|
|
||||||
|
# project commands
|
||||||
|
def add_project(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
|
data: Dict[str, str] = {}
|
||||||
|
data["desc"] = {}
|
||||||
|
|
||||||
|
data["name"] = self.log.input("Project name")
|
||||||
|
|
||||||
|
for lang in self.api.languages:
|
||||||
|
data["desc"][lang] = self.log.input("Project desc (%s)" % lang)
|
||||||
|
|
||||||
|
data["url"] = self.log.input("Project URL")
|
||||||
|
data["license"] = self.log.input("Project license")
|
||||||
|
|
||||||
|
self.api.add_project(data)
|
||||||
|
self.log.info("Project has been added")
|
||||||
|
|
||||||
|
def del_project(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
|
data: Dict[str, str] = {}
|
||||||
|
data["name"] = self.log.input("Project name")
|
||||||
|
|
||||||
|
self.api.del_project(data["name"])
|
||||||
|
self.log.info("Project has been deleted")
|
||||||
|
|
||||||
|
# news command
|
||||||
|
def add_news(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
|
news: Dict[str, str] = {}
|
||||||
|
news["title"] = {}
|
||||||
|
news["content"] = {}
|
||||||
|
|
||||||
|
data["id"] = self.log.input("News ID")
|
||||||
|
|
||||||
|
for lang in self.api.languages:
|
||||||
|
data["title"][lang] = self.log.input("News title (%s)" % lang)
|
||||||
|
|
||||||
|
data["author"] = self.log.input("News author")
|
||||||
|
|
||||||
|
for lang in self.api.languages:
|
||||||
|
data["content"][lang] = self.log.input("News content (%s)" % lang)
|
||||||
|
|
||||||
|
self.api.add_news(data)
|
||||||
|
self.log.info("News has been added")
|
||||||
|
|
||||||
|
def del_news(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
if data is None:
|
||||||
|
data: Dict[str, str] = {}
|
||||||
|
data["id"] = self.log.input("News ID")
|
||||||
|
|
||||||
|
self.api.del_project(data["id"])
|
||||||
|
self.log.info("News has been deleted")
|
||||||
|
|
||||||
|
def check_services(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
self.api.check_services()
|
||||||
|
self.log.info("Requested status check for all the services")
|
||||||
|
|
||||||
|
def get_logs(self, data: Dict[str, Any] = None) -> None:
|
||||||
|
logs = self.api.logs()
|
||||||
|
|
||||||
|
if logs["result"] is None or len(logs["result"]) == 0:
|
||||||
|
return self.log.info("No available logs")
|
||||||
|
|
||||||
|
for log in logs["result"]:
|
||||||
|
self.log.info(
|
||||||
|
"Time: %s | Action: %s"
|
||||||
|
% (self.__format_time(log["time"]), log["action"])
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_command(self, cmd: str, file: str = None) -> bool:
|
||||||
|
for command in self.commands.keys():
|
||||||
|
if command != cmd:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if file != "" and file is not None:
|
||||||
|
data = self.__load_json_file(file)
|
||||||
|
|
||||||
|
self.commands[cmd](data)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error("Command failed: %s" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log.error("Invalid command: %s", cmd)
|
||||||
|
return False
|
||||||
|
|
||||||
print("[-] Command not found")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
script = AdminScript()
|
||||||
|
exit(script.run() if 1 else 0)
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
echo -n "Enter API URL: "
|
|
||||||
read url
|
|
||||||
cat > /usr/bin/admin_script << EOF
|
|
||||||
#!/bin/sh
|
|
||||||
API=$url python3 $(pwd)/admin.py \$1
|
|
||||||
EOF
|
|
||||||
chmod +x /usr/bin/admin_script
|
|
12
admin/tests/test_news.json
Normal file
12
admin/tests/test_news.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "test_news",
|
||||||
|
"title": {
|
||||||
|
"en": "Very important news",
|
||||||
|
"tr": "Çok önemli haber"
|
||||||
|
},
|
||||||
|
"author": "ngn",
|
||||||
|
"content": {
|
||||||
|
"en": "Just letting you know that I'm testing the API",
|
||||||
|
"tr": "Sadece API'ı test ettiğimi bilmenizi istedim"
|
||||||
|
}
|
||||||
|
}
|
9
admin/tests/test_project.json
Normal file
9
admin/tests/test_project.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"desc": {
|
||||||
|
"en": "A non-existent project used to test the API",
|
||||||
|
"tr": "API'ı test etmek için kullanılan varolmayan bir proje"
|
||||||
|
},
|
||||||
|
"url": "https://github.com/ngn13/test",
|
||||||
|
"license": "GPL-3.0"
|
||||||
|
}
|
9
admin/tests/test_service.json
Normal file
9
admin/tests/test_service.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Test Service",
|
||||||
|
"desc": {
|
||||||
|
"en": "Service used for testing the API",
|
||||||
|
"tr": "API'ı test etmek için kullanılan servis"
|
||||||
|
},
|
||||||
|
"check_url": "http://localhost:7001",
|
||||||
|
"clear": "http://localhost:7001"
|
||||||
|
}
|
2
api/.gitignore
vendored
2
api/.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
server
|
*.elf
|
||||||
*.db
|
*.db
|
||||||
|
@ -1,17 +1,26 @@
|
|||||||
FROM golang:1.23.4
|
FROM golang:1.23.4
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /api
|
||||||
|
|
||||||
|
RUN useradd runner -r -u 1001 -d /api
|
||||||
|
RUN chown -R runner:runner /api
|
||||||
|
USER runner
|
||||||
|
|
||||||
COPY *.go ./
|
|
||||||
COPY *.mod ./
|
COPY *.mod ./
|
||||||
COPY *.sum ./
|
COPY *.sum ./
|
||||||
|
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY *.go ./
|
||||||
COPY Makefile ./
|
COPY Makefile ./
|
||||||
COPY routes ./routes
|
|
||||||
COPY config ./config
|
COPY config ./config
|
||||||
COPY database ./database
|
COPY database ./database
|
||||||
|
COPY routes ./routes
|
||||||
|
COPY sql ./sql
|
||||||
|
COPY status ./status
|
||||||
COPY util ./util
|
COPY util ./util
|
||||||
|
COPY views ./views
|
||||||
|
|
||||||
EXPOSE 7001
|
|
||||||
RUN make
|
RUN make
|
||||||
|
|
||||||
ENTRYPOINT ["/app/server"]
|
ENTRYPOINT ["/api/api.elf"]
|
||||||
|
12
api/Makefile
12
api/Makefile
@ -1,10 +1,12 @@
|
|||||||
all: server
|
GOSRCS = $(wildcard *.go) $(wildcard */*.go)
|
||||||
|
|
||||||
server: *.go routes/*.go database/*.go util/*.go config/*.go
|
all: api.elf
|
||||||
go build -o $@ .
|
|
||||||
|
|
||||||
test:
|
api.elf: $(GOSRCS)
|
||||||
API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./server
|
go build -o $@
|
||||||
|
|
||||||
|
run:
|
||||||
|
WEBSITE_DEBUG=true WEBSITE_PASSWORD=test ./api.elf
|
||||||
|
|
||||||
format:
|
format:
|
||||||
gofmt -s -w .
|
gofmt -s -w .
|
||||||
|
@ -2,59 +2,108 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ngn13/website/api/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Option struct {
|
type Type struct {
|
||||||
Name string
|
Options []Option
|
||||||
Value string
|
Count int
|
||||||
Required bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Option) Env() string {
|
func (c *Type) Find(name string, typ uint8) (*Option, error) {
|
||||||
return strings.ToUpper(fmt.Sprintf("API_%s", o.Name))
|
for i := 0; i < c.Count; i++ {
|
||||||
}
|
if c.Options[i].Name != name {
|
||||||
|
|
||||||
var options []Option = []Option{
|
|
||||||
{Name: "password", Value: "", Required: true},
|
|
||||||
{Name: "frontend_url", Value: "http://localhost:5173/", Required: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
func Load() bool {
|
|
||||||
var val string
|
|
||||||
|
|
||||||
for i := range options {
|
|
||||||
if val = os.Getenv(options[i].Env()); val == "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
options[i].Value = val
|
if c.Options[i].Type != typ {
|
||||||
options[i].Required = false
|
return nil, fmt.Errorf("bad option type")
|
||||||
}
|
|
||||||
|
|
||||||
for i := range options {
|
|
||||||
if options[i].Required && options[i].Value == "" {
|
|
||||||
util.Fail("please specify the required config option \"%s\" (\"%s\")", options[i].Name, options[i].Env())
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if options[i].Required && options[i].Value != "" {
|
return &c.Options[i], nil
|
||||||
util.Fail("using the default value \"%s\" for required config option \"%s\" (\"%s\")", options[i].Value, options[i].Name, options[i].Env())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return nil, fmt.Errorf("option not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(name string) string {
|
func (c *Type) Load() (err error) {
|
||||||
for i := range options {
|
var (
|
||||||
if options[i].Name != name {
|
env_val string
|
||||||
continue
|
env_name string
|
||||||
}
|
opt *Option
|
||||||
return options[i].Value
|
exists bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// default options
|
||||||
|
c.Options = []Option{
|
||||||
|
{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
|
||||||
|
{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"
|
||||||
}
|
}
|
||||||
return ""
|
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
49
api/config/option.go
Normal 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
|
||||||
|
}
|
62
api/database/admin_log.go
Normal file
62
api/database/admin_log.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminLog struct {
|
||||||
|
Action string `json:"action"` // action that was performed (service removal, service addition etc.)
|
||||||
|
Time int64 `json:"time"` // time when the action was performed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AdminLog) Scan(rows *sql.Rows) (err error) {
|
||||||
|
if rows != nil {
|
||||||
|
return rows.Scan(&l.Action, &l.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no row/rows specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) AdminLogNext(l *AdminLog) bool {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if nil == db.rows {
|
||||||
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_ADMIN_LOG); err != nil {
|
||||||
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !db.rows.Next() {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = l.Scan(db.rows); err != nil {
|
||||||
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if db.rows != nil {
|
||||||
|
db.rows.Close()
|
||||||
|
}
|
||||||
|
db.rows = nil
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) AdminLogAdd(l *AdminLog) error {
|
||||||
|
_, err := db.sql.Exec(
|
||||||
|
"INSERT INTO "+TABLE_ADMIN_LOG+`(
|
||||||
|
action, time
|
||||||
|
) values(?, ?)`,
|
||||||
|
&l.Action, &l.Time,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
@ -1,44 +1,64 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Setup(db *sql.DB) error {
|
const (
|
||||||
_, err := db.Exec(`
|
SQL_PATH = "sql"
|
||||||
CREATE TABLE IF NOT EXISTS posts(
|
|
||||||
id TEXT NOT NULL UNIQUE,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
author TEXT NOT NULL,
|
|
||||||
date TEXT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
public INTEGER NOT NULL,
|
|
||||||
vote INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
`)
|
|
||||||
|
|
||||||
if err != nil {
|
TABLE_ADMIN_LOG = "admin_log" // stores administrator logs
|
||||||
return err
|
TABLE_METRICS = "metrics" // stores API usage metrcis
|
||||||
}
|
TABLE_NEWS = "news" // stores news posts
|
||||||
|
TABLE_SERVICES = "services" // stores services
|
||||||
|
TABLE_PROJECTS = "projects" // stores projects
|
||||||
|
)
|
||||||
|
|
||||||
_, err = db.Exec(`
|
var tables []string = []string{
|
||||||
CREATE TABLE IF NOT EXISTS services(
|
TABLE_ADMIN_LOG, TABLE_METRICS, TABLE_NEWS,
|
||||||
name TEXT NOT NULL UNIQUE,
|
TABLE_SERVICES, TABLE_PROJECTS,
|
||||||
desc TEXT NOT NULL,
|
}
|
||||||
url TEXT NOT NULL
|
|
||||||
);
|
type Type struct {
|
||||||
`)
|
sql *sql.DB
|
||||||
|
rows *sql.Rows
|
||||||
if err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
func (db *Type) create_table(table string) error {
|
||||||
|
var (
|
||||||
_, err = db.Exec(`
|
err error
|
||||||
CREATE TABLE IF NOT EXISTS votes(
|
query []byte
|
||||||
hash TEXT NOT NULL UNIQUE,
|
)
|
||||||
is_upvote INTEGER NOT NULL
|
|
||||||
);
|
query_path := path.Join(SQL_PATH, table+".sql")
|
||||||
`)
|
|
||||||
|
if query, err = os.ReadFile(query_path); err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to read %s for table %s: %", query_path, table, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = db.sql.Exec(string(query)); err != nil {
|
||||||
|
return fmt.Errorf("failed to create the %s table: %s", table, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) Load() (err error) {
|
||||||
|
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||||
|
return fmt.Errorf("failed access the database: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
if err = db.create_table(table); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
57
api/database/metrics.go
Normal file
57
api/database/metrics.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *Type) MetricsGet(key string) (uint64, error) {
|
||||||
|
var (
|
||||||
|
row *sql.Row
|
||||||
|
count uint64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if row = db.sql.QueryRow("SELECT value FROM "+TABLE_METRICS+" WHERE key = ?", key); row == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = row.Scan(&count); err != nil && err != sql.ErrNoRows {
|
||||||
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) MetricsSet(key string, value uint64) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
res sql.Result
|
||||||
|
)
|
||||||
|
|
||||||
|
if res, err = db.sql.Exec("UPDATE "+TABLE_METRICS+" SET value = ? WHERE key = ?", value, key); err != nil && err != sql.ErrNoRows {
|
||||||
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if effected, err := res.RowsAffected(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if effected < 1 {
|
||||||
|
_, err = db.sql.Exec(
|
||||||
|
"INSERT INTO "+TABLE_METRICS+`(
|
||||||
|
key, value
|
||||||
|
) values(?, ?)`,
|
||||||
|
key, value,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
58
api/database/multilang.go
Normal file
58
api/database/multilang.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Multilang struct {
|
||||||
|
En string `json:"en"` // english
|
||||||
|
Tr string `json:"tr"` // turkish
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Supports(lang string) bool {
|
||||||
|
ml_ref := reflect.ValueOf(ml).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < reflect.Indirect(ml_ref).NumField(); i++ {
|
||||||
|
if name := reflect.Indirect(ml_ref).Type().Field(i).Name; strings.ToLower(name) == lang {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Get(lang string) string {
|
||||||
|
r := []rune(lang)
|
||||||
|
r[0] = unicode.ToUpper(r[0])
|
||||||
|
l := string(r)
|
||||||
|
|
||||||
|
ml_ref := reflect.ValueOf(ml)
|
||||||
|
return reflect.Indirect(ml_ref).FieldByName(l).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Empty() bool {
|
||||||
|
ml_ref := reflect.ValueOf(ml)
|
||||||
|
|
||||||
|
for i := 0; i < reflect.Indirect(ml_ref).NumField(); i++ {
|
||||||
|
if field := reflect.Indirect(ml_ref).Field(i); field.String() != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Dump() (string, error) {
|
||||||
|
if data, err := json.Marshal(ml); err != nil {
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *Multilang) Load(s string) error {
|
||||||
|
return json.Unmarshal([]byte(s), ml)
|
||||||
|
}
|
116
api/database/news.go
Normal file
116
api/database/news.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type News struct {
|
||||||
|
ID string `json:"id"` // ID of the news
|
||||||
|
title string `json:"-"` // title of the news (string)
|
||||||
|
Title Multilang `json:"title"` // title of the news
|
||||||
|
Author string `json:"author"` // author of the news
|
||||||
|
Time uint64 `json:"time"` // when the new was published
|
||||||
|
content string `json:"-"` // content of the news (string)
|
||||||
|
Content Multilang `json:"content"` // content of the news
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) Supports(lang string) bool {
|
||||||
|
return n.Content.Supports(lang) && n.Title.Supports(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) Load() (err error) {
|
||||||
|
if err = n.Title.Load(n.title); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = n.Content.Load(n.content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) Dump() (err error) {
|
||||||
|
if n.title, err = n.Title.Dump(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.content, err = n.Content.Dump(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) Scan(rows *sql.Rows) (err error) {
|
||||||
|
err = rows.Scan(
|
||||||
|
&n.ID, &n.title, &n.Author,
|
||||||
|
&n.Time, &n.content)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *News) IsValid() bool {
|
||||||
|
return n.Time != 0 && n.Author != "" && n.ID != "" && !n.Title.Empty() && !n.Content.Empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) NewsNext(n *News) bool {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if nil == db.rows {
|
||||||
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_NEWS); err != nil {
|
||||||
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !db.rows.Next() {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = n.Scan(db.rows); err != nil {
|
||||||
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if db.rows != nil {
|
||||||
|
db.rows.Close()
|
||||||
|
}
|
||||||
|
db.rows = nil
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) NewsRemove(id string) error {
|
||||||
|
_, err := db.sql.Exec(
|
||||||
|
"DELETE FROM "+TABLE_NEWS+" WHERE id = ?",
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) NewsAdd(n *News) (err error) {
|
||||||
|
if err = n.Dump(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.sql.Exec(
|
||||||
|
"INSERT OR REPLACE INTO "+TABLE_NEWS+`(
|
||||||
|
id, title, author, time, content
|
||||||
|
) values(?, ?, ?, ?, ?)`,
|
||||||
|
n.ID, n.title,
|
||||||
|
n.Author, n.Time, n.content,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
@ -1,71 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/ngn13/website/api/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Post struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
Date string `json:"date"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
Public int `json:"public"`
|
|
||||||
Vote int `json:"vote"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Load(rows *sql.Rows) error {
|
|
||||||
return rows.Scan(&p.ID, &p.Title, &p.Author, &p.Date, &p.Content, &p.Public, &p.Vote)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Get(db *sql.DB, id string) (bool, error) {
|
|
||||||
var (
|
|
||||||
success bool
|
|
||||||
rows *sql.Rows
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM posts WHERE id = ?", id); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
if success = rows.Next(); !success {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = p.Load(rows); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Remove(db *sql.DB) error {
|
|
||||||
_, err := db.Exec("DELETE FROM posts WHERE id = ?", p.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Save(db *sql.DB) error {
|
|
||||||
p.ID = util.TitleToID(p.Title)
|
|
||||||
|
|
||||||
_, err := db.Exec(
|
|
||||||
"INSERT INTO posts(id, title, author, date, content, public, vote) values(?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
p.ID, p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote,
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) Update(db *sql.DB) error {
|
|
||||||
p.ID = util.TitleToID(p.Title)
|
|
||||||
|
|
||||||
_, err := db.Exec(
|
|
||||||
"UPDATE posts SET title = ?, author = ?, date = ?, content = ?, public = ?, vote = ? WHERE id = ?",
|
|
||||||
p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote, p.ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
92
api/database/project.go
Normal file
92
api/database/project.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
Name string `json:"name"` // name of the project
|
||||||
|
desc string `json:"-"` // description of the project (string)
|
||||||
|
Desc Multilang `json:"desc"` // description of the project
|
||||||
|
URL string `json:"url"` // URL of the project's homepage/source
|
||||||
|
License string `json:"license"` // name of project's license
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) Load() error {
|
||||||
|
return p.Desc.Load(p.desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) Dump() (err error) {
|
||||||
|
p.desc, err = p.Desc.Dump()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) Scan(rows *sql.Rows) (err error) {
|
||||||
|
if err = rows.Scan(
|
||||||
|
&p.Name, &p.desc,
|
||||||
|
&p.URL, &p.License); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Project) IsValid() bool {
|
||||||
|
return p.Name != "" && p.URL != "" && !p.Desc.Empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ProjectNext(p *Project) bool {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if nil == db.rows {
|
||||||
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_PROJECTS); err != nil {
|
||||||
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !db.rows.Next() {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.Scan(db.rows); err != nil {
|
||||||
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if db.rows != nil {
|
||||||
|
db.rows.Close()
|
||||||
|
}
|
||||||
|
db.rows = nil
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ProjectRemove(name string) error {
|
||||||
|
_, err := db.sql.Exec(
|
||||||
|
"DELETE FROM "+TABLE_PROJECTS+" WHERE name = ?",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ProjectAdd(p *Project) (err error) {
|
||||||
|
if err = p.Dump(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.sql.Exec(
|
||||||
|
"INSERT OR REPLACE INTO "+TABLE_PROJECTS+`(
|
||||||
|
name, desc, url, license
|
||||||
|
) values(?, ?, ?, ?)`,
|
||||||
|
p.Name, p.desc, p.URL, p.License,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
@ -2,54 +2,127 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"` // name of the service
|
||||||
Desc string `json:"desc"`
|
desc string `json:"-"` // description of the service (string)
|
||||||
Url string `json:"url"`
|
Desc Multilang `json:"desc"` // description of the service
|
||||||
|
CheckTime uint64 `json:"check_time"` // last status check time
|
||||||
|
CheckRes uint8 `json:"check_res"` // result of the status check
|
||||||
|
CheckURL string `json:"check_url"` // URL used for status check
|
||||||
|
Clear string `json:"clear"` // Clearnet (cringe) URL for the service
|
||||||
|
Onion string `json:"onion"` // Onion (TOR) URL for the service
|
||||||
|
I2P string `json:"i2p"` // I2P URL for the service
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Load(rows *sql.Rows) error {
|
func (s *Service) Load() error {
|
||||||
return rows.Scan(&s.Name, &s.Desc, &s.Url)
|
return s.Desc.Load(s.desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Get(db *sql.DB, name string) (bool, error) {
|
func (s *Service) Dump() (err error) {
|
||||||
|
s.desc, err = s.Desc.Dump()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Scan(rows *sql.Rows, row *sql.Row) (err error) {
|
||||||
|
if rows != nil {
|
||||||
|
err = rows.Scan(
|
||||||
|
&s.Name, &s.desc,
|
||||||
|
&s.CheckTime, &s.CheckRes, &s.CheckURL,
|
||||||
|
&s.Clear, &s.Onion, &s.I2P)
|
||||||
|
} else if row != nil {
|
||||||
|
err = row.Scan(
|
||||||
|
&s.Name, &s.desc,
|
||||||
|
&s.CheckTime, &s.CheckRes, &s.CheckURL,
|
||||||
|
&s.Clear, &s.Onion, &s.I2P)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("no row/rows specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) IsValid() bool {
|
||||||
|
return s.Name != "" && (s.Clear != "" || s.Onion != "" || s.I2P != "") && !s.Desc.Empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ServiceNext(s *Service) bool {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if nil == db.rows {
|
||||||
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_SERVICES); err != nil {
|
||||||
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !db.rows.Next() {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.Scan(db.rows, nil); err != nil {
|
||||||
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
fail:
|
||||||
|
if db.rows != nil {
|
||||||
|
db.rows.Close()
|
||||||
|
}
|
||||||
|
db.rows = nil
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Type) ServiceFind(name string) (*Service, error) {
|
||||||
var (
|
var (
|
||||||
success bool
|
row *sql.Row
|
||||||
rows *sql.Rows
|
s Service
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM services WHERE name = ?", name); err != nil {
|
if row = db.sql.QueryRow("SELECT * FROM "+TABLE_SERVICES+" WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows {
|
||||||
return false, err
|
return nil, nil
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
if success = rows.Next(); !success {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.Load(rows); err != nil {
|
if err = s.Scan(nil, row); err != nil {
|
||||||
return false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return &s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Remove(db *sql.DB) error {
|
func (db *Type) ServiceRemove(name string) error {
|
||||||
_, err := db.Exec(
|
_, err := db.sql.Exec(
|
||||||
"DELETE FROM services WHERE name = ?",
|
"DELETE FROM "+TABLE_SERVICES+" WHERE name = ?",
|
||||||
s.Name,
|
name,
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Save(db *sql.DB) error {
|
func (db *Type) ServiceUpdate(s *Service) (err error) {
|
||||||
_, err := db.Exec(
|
if err = s.Dump(); err != nil {
|
||||||
"INSERT INTO services(name, desc, url) values(?, ?, ?)",
|
return err
|
||||||
s.Name, s.Desc, s.Url,
|
}
|
||||||
|
|
||||||
|
_, err = db.sql.Exec(
|
||||||
|
"INSERT OR REPLACE INTO "+TABLE_SERVICES+`(
|
||||||
|
name, desc, check_time, check_res, check_url, clear, onion, i2p
|
||||||
|
) values(?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
s.Name, s.desc,
|
||||||
|
s.CheckTime, s.CheckRes, s.CheckURL,
|
||||||
|
s.Clear, s.Onion, s.I2P,
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import "database/sql"
|
|
||||||
|
|
||||||
type Vote struct {
|
|
||||||
Hash string
|
|
||||||
IsUpvote bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vote) Load(rows *sql.Rows) error {
|
|
||||||
return rows.Scan(&v.Hash, &v.IsUpvote)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vote) Get(db *sql.DB, hash string) (bool, error) {
|
|
||||||
var (
|
|
||||||
success bool
|
|
||||||
rows *sql.Rows
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM votes WHERE hash = ?", hash); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
if success = rows.Next(); !success {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = v.Load(rows); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vote) Update(db *sql.DB) error {
|
|
||||||
_, err := db.Exec("UPDATE votes SET is_upvote = ? WHERE hash = ?", v.IsUpvote, v.Hash)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vote) Save(db *sql.DB) error {
|
|
||||||
_, err := db.Exec(
|
|
||||||
"INSERT INTO votes(hash, is_upvote) values(?, ?)",
|
|
||||||
v.Hash, v.IsUpvote,
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ go 1.21.3
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gofiber/fiber/v2 v2.52.5
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
github.com/gorilla/feeds v1.2.0
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,14 +4,8 @@ github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yG
|
|||||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
|
|
||||||
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
|
||||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
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=
|
||||||
@ -23,8 +17,6 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
|
|||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||||
|
127
api/main.go
127
api/main.go
@ -1,102 +1,131 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
* website/api | API server for my personal website
|
||||||
|
* written by ngn (https://ngn.tf) (2025)
|
||||||
|
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/ngn13/website/api/config"
|
"github.com/ngn13/website/api/config"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
"github.com/ngn13/website/api/routes"
|
"github.com/ngn13/website/api/routes"
|
||||||
|
"github.com/ngn13/website/api/status"
|
||||||
"github.com/ngn13/website/api/util"
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var db *sql.DB
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
app *fiber.App
|
app *fiber.App
|
||||||
//db *sql.DB
|
stat status.Type
|
||||||
|
|
||||||
|
conf config.Type
|
||||||
|
db database.Type
|
||||||
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if !config.Load() {
|
if err = conf.Load(); err != nil {
|
||||||
util.Fail("failed to load the configuration")
|
util.Fail("failed to load the configuration: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if db, err = sql.Open("sqlite3", "data.db"); err != nil {
|
if !conf.GetBool("debug") {
|
||||||
util.Fail("cannot access the database: %s", err.Error())
|
util.Debg = func(m string, v ...any) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.Load(); err != nil {
|
||||||
|
util.Fail("failed to load the database: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
if err = database.Setup(db); err != nil {
|
if err = stat.Setup(&conf, &db); err != nil {
|
||||||
util.Fail("cannot setup the database: %s", err.Error())
|
util.Fail("failed to setup the status checker: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app = fiber.New(fiber.Config{
|
app = fiber.New(fiber.Config{
|
||||||
|
AppName: "ngn's website",
|
||||||
DisableStartupMessage: true,
|
DisableStartupMessage: true,
|
||||||
|
ServerHeader: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Use("*", func(c *fiber.Ctx) error {
|
app.Use("*", func(c *fiber.Ctx) error {
|
||||||
|
// CORS stuff
|
||||||
c.Set("Access-Control-Allow-Origin", "*")
|
c.Set("Access-Control-Allow-Origin", "*")
|
||||||
c.Set("Access-Control-Allow-Headers",
|
c.Set("Access-Control-Allow-Headers",
|
||||||
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||||
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET")
|
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET") // POST can be sent from HTML forms, so I prefer PUT for API endpoints
|
||||||
|
|
||||||
|
c.Locals("status", &stat)
|
||||||
|
c.Locals("config", &conf)
|
||||||
c.Locals("database", &db)
|
c.Locals("database", &db)
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
// index route
|
// index route
|
||||||
app.Get("/", func(c *fiber.Ctx) error {
|
app.Get("/", routes.GET_Index)
|
||||||
return c.Send([]byte("o/"))
|
|
||||||
})
|
|
||||||
|
|
||||||
// blog routes
|
// version groups
|
||||||
blog_routes := app.Group("/blog")
|
v1 := app.Group("v1")
|
||||||
|
|
||||||
// blog feed routes
|
// v1 user routes
|
||||||
blog_routes.Get("/feed.*", routes.GET_Feed)
|
v1.Get("/services", routes.GET_Services)
|
||||||
|
v1.Get("/projects", routes.GET_Projects)
|
||||||
|
v1.Get("/metrics", routes.GET_Metrics)
|
||||||
|
v1.Get("/news/:lang", routes.GET_News)
|
||||||
|
|
||||||
// blog post routes
|
// v1 admin routes
|
||||||
blog_routes.Get("/sum", routes.GET_PostSum)
|
v1.Use("/admin", routes.AuthMiddleware)
|
||||||
blog_routes.Get("/get", routes.GET_Post)
|
v1.Get("/admin/logs", routes.GET_AdminLogs)
|
||||||
|
|
||||||
// blog post vote routes
|
v1.Get("/admin/service/check", routes.GET_CheckService)
|
||||||
blog_routes.Get("/vote/set", routes.GET_VoteSet)
|
v1.Put("/admin/service/add", routes.PUT_AddService)
|
||||||
blog_routes.Get("/vote/get", routes.GET_VoteGet)
|
v1.Delete("/admin/service/del", routes.DEL_DelService)
|
||||||
|
|
||||||
// service routes
|
v1.Put("/admin/project/add", routes.PUT_AddProject)
|
||||||
service_routes := app.Group("services")
|
v1.Delete("/admin/project/del", routes.DEL_DelProject)
|
||||||
service_routes.Get("/all", routes.GET_Services)
|
|
||||||
|
|
||||||
// admin routes
|
v1.Put("/admin/news/add", routes.PUT_AddNews)
|
||||||
admin_routes := app.Group("admin")
|
v1.Delete("/admin/news/del", routes.DEL_DelNews)
|
||||||
admin_routes.Use("*", routes.AuthMiddleware)
|
|
||||||
|
|
||||||
// admin auth routes
|
|
||||||
admin_routes.Get("/login", routes.GET_Login)
|
|
||||||
admin_routes.Get("/logout", routes.GET_Logout)
|
|
||||||
|
|
||||||
// admin service managment routes
|
|
||||||
admin_routes.Put("/service/add", routes.PUT_AddService)
|
|
||||||
admin_routes.Delete("/service/remove", routes.DEL_RemoveService)
|
|
||||||
|
|
||||||
// admin blog managment routes
|
|
||||||
admin_routes.Put("/blog/add", routes.PUT_AddPost)
|
|
||||||
admin_routes.Delete("/blog/remove", routes.DEL_RemovePost)
|
|
||||||
|
|
||||||
// 404 route
|
// 404 route
|
||||||
app.All("*", func(c *fiber.Ctx) error {
|
app.All("*", func(c *fiber.Ctx) error {
|
||||||
return util.ErrNotFound(c)
|
return util.JSON(c, http.StatusNotFound, fiber.Map{
|
||||||
|
"error": "Endpoint not found",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
util.Info("starting web server at port 7001")
|
// start the status checker
|
||||||
|
if err = stat.Run(); err != nil {
|
||||||
if err = app.Listen("0.0.0.0:7001"); err != nil {
|
util.Fail("failed to start the status checker: %s", err.Error())
|
||||||
util.Fail("error starting the webserver: %s", err.Error())
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start the app
|
||||||
|
util.Info("starting web server on %s", conf.GetStr("host"))
|
||||||
|
|
||||||
|
if err = app.Listen(conf.GetStr("host")); err != nil {
|
||||||
|
util.Fail("failed to start the web server: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
stat.Stop()
|
||||||
}
|
}
|
||||||
|
@ -1,183 +1,206 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
"github.com/ngn13/website/api/config"
|
"github.com/ngn13/website/api/config"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/status"
|
||||||
"github.com/ngn13/website/api/util"
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Token string = util.CreateToken()
|
func admin_log(c *fiber.Ctx, m string) error {
|
||||||
|
return c.Locals("database").(*database.Type).AdminLogAdd(&database.AdminLog{
|
||||||
|
Action: m, // action that the admin peformed
|
||||||
|
Time: time.Now().Unix(), // current time
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AuthMiddleware(c *fiber.Ctx) error {
|
func AuthMiddleware(c *fiber.Ctx) error {
|
||||||
if c.Path() == "/admin/login" {
|
conf := c.Locals("config").(*config.Type)
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Get("Authorization") != Token {
|
if c.Get("Authorization") != conf.GetStr("password") {
|
||||||
return util.ErrAuth(c)
|
return util.ErrAuth(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GET_Login(c *fiber.Ctx) error {
|
func GET_AdminLogs(c *fiber.Ctx) error {
|
||||||
if c.Query("pass") != config.Get("password") {
|
|
||||||
return util.ErrAuth(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
util.Info("new login from %s", util.GetIP(c))
|
|
||||||
|
|
||||||
return c.Status(http.StatusOK).JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"token": Token,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_Logout(c *fiber.Ctx) error {
|
|
||||||
Token = util.CreateToken()
|
|
||||||
|
|
||||||
util.Info("logout from %s", util.GetIP(c))
|
|
||||||
|
|
||||||
return c.Status(http.StatusOK).JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func DEL_RemoveService(c *fiber.Ctx) error {
|
|
||||||
var (
|
var (
|
||||||
db *sql.DB
|
list []database.AdminLog
|
||||||
service database.Service
|
log database.AdminLog
|
||||||
name string
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
db := c.Locals("database").(*database.Type)
|
||||||
name = c.Query("name")
|
|
||||||
|
|
||||||
if name == "" {
|
for db.AdminLogNext(&log) {
|
||||||
util.ErrBadData(c)
|
list = append(list, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
if found, err = service.Get(db, name); err != nil {
|
return util.JSON(c, 200, fiber.Map{
|
||||||
util.Fail("error while searching for a service (\"%s\"): %s", name, err.Error())
|
"result": list,
|
||||||
return util.ErrServer(c)
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DEL_DelService(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
if name = c.Query("name"); name == "" {
|
||||||
|
util.ErrBadReq(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if err = admin_log(c, fmt.Sprintf("Removed service \"%s\"", name)); err != nil {
|
||||||
return util.ErrEntryNotExists(c)
|
return util.ErrInternal(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = service.Remove(db); err != nil {
|
if err = db.ServiceRemove(name); err != nil {
|
||||||
util.Fail("error while removing a service (\"%s\"): %s", service.Name, err.Error())
|
return util.ErrInternal(c, err)
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.JSON(c, 200, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PUT_AddService(c *fiber.Ctx) error {
|
func PUT_AddService(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
service database.Service
|
service database.Service
|
||||||
db *sql.DB
|
|
||||||
found bool
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
if c.BodyParser(&service) != nil {
|
if c.BodyParser(&service) != nil {
|
||||||
return util.ErrBadJSON(c)
|
return util.ErrBadJSON(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if service.Name == "" || service.Desc == "" || service.Url == "" {
|
if !service.IsValid() {
|
||||||
return util.ErrBadData(c)
|
return util.ErrBadReq(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if found, err = service.Get(db, service.Name); err != nil {
|
if err = admin_log(c, fmt.Sprintf("Added service \"%s\"", service.Name)); err != nil {
|
||||||
util.Fail("error while searching for a service (\"%s\"): %s", service.Name, err.Error())
|
return util.ErrInternal(c, err)
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if found {
|
if err = db.ServiceUpdate(&service); err != nil {
|
||||||
return util.ErrEntryExists(c)
|
return util.ErrInternal(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = service.Save(db); err != nil {
|
// force a status check so we can get the status of the new service
|
||||||
util.Fail("error while saving a new service (\"%s\"): %s", service.Name, err.Error())
|
c.Locals("status").(*status.Type).Check()
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.JSON(c, 200, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DEL_RemovePost(c *fiber.Ctx) error {
|
func GET_CheckService(c *fiber.Ctx) error {
|
||||||
var (
|
c.Locals("status").(*status.Type).Check()
|
||||||
db *sql.DB
|
return util.JSON(c, 200, nil)
|
||||||
id string
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
post database.Post
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
|
|
||||||
if id = c.Query("id"); id == "" {
|
|
||||||
return util.ErrBadData(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if found, err = post.Get(db, id); err != nil {
|
|
||||||
util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return util.ErrEntryNotExists(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = post.Remove(db); err != nil {
|
|
||||||
util.Fail("error while removing a post (\"%s\"): %s", post.ID, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.NoError(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PUT_AddPost(c *fiber.Ctx) error {
|
func PUT_AddProject(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
db *sql.DB
|
project database.Project
|
||||||
post database.Post
|
err error
|
||||||
err error
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
db := c.Locals("database").(*database.Type)
|
||||||
post.Public = 1
|
|
||||||
|
|
||||||
if c.BodyParser(&post) != nil {
|
if c.BodyParser(&project) != nil {
|
||||||
return util.ErrBadJSON(c)
|
return util.ErrBadJSON(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if post.Title == "" || post.Author == "" || post.Content == "" {
|
if !project.IsValid() {
|
||||||
return util.ErrBadData(c)
|
return util.ErrBadReq(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
post.Date = time.Now().Format("02/01/06")
|
if err = admin_log(c, fmt.Sprintf("Added project \"%s\"", project.Name)); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
if err = post.Save(db); err != nil && strings.Contains(err.Error(), sqlite3.ErrConstraintUnique.Error()) {
|
|
||||||
return util.ErrEntryExists(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err = db.ProjectAdd(&project); err != nil {
|
||||||
util.Fail("error while saving a new post (\"%s\"): %s", post.ID, err.Error())
|
return util.ErrInternal(c, err)
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.JSON(c, 200, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DEL_DelProject(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
if name = c.Query("name"); name == "" {
|
||||||
|
util.ErrBadReq(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = admin_log(c, fmt.Sprintf("Removed project \"%s\"", name)); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.ProjectRemove(name); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DEL_DelNews(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
id string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
if id = c.Query("id"); id == "" {
|
||||||
|
util.ErrBadReq(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = admin_log(c, fmt.Sprintf("Removed news \"%s\"", id)); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.NewsRemove(id); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PUT_AddNews(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
news database.News
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
if c.BodyParser(&news) != nil {
|
||||||
|
return util.ErrBadJSON(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
news.Time = uint64(time.Now().Unix())
|
||||||
|
|
||||||
|
if !news.IsValid() {
|
||||||
|
return util.ErrBadReq(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = admin_log(c, fmt.Sprintf("Added news \"%s\"", news.ID)); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.NewsAdd(&news); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, nil)
|
||||||
}
|
}
|
||||||
|
@ -1,203 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gorilla/feeds"
|
|
||||||
"github.com/ngn13/website/api/config"
|
|
||||||
"github.com/ngn13/website/api/database"
|
|
||||||
"github.com/ngn13/website/api/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GET_Post(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
post database.Post
|
|
||||||
id string
|
|
||||||
db *sql.DB
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
|
|
||||||
if id = c.Query("id"); id == "" {
|
|
||||||
return util.ErrBadData(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if found, err = post.Get(db, id); err != nil {
|
|
||||||
util.Fail("error while search for a post (\"%s\"): %s", id, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return util.ErrEntryNotExists(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"result": post,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_PostSum(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
posts []database.Post
|
|
||||||
rows *sql.Rows
|
|
||||||
db *sql.DB
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM posts"); err != nil {
|
|
||||||
util.Fail("cannot load posts: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var post database.Post
|
|
||||||
|
|
||||||
if err = post.Load(rows); err != nil {
|
|
||||||
util.Fail("error while loading post: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Public == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(post.Content) > 255 {
|
|
||||||
post.Content = post.Content[0:250]
|
|
||||||
}
|
|
||||||
|
|
||||||
posts = append(posts, post)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"result": posts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFeed(db *sql.DB) (*feeds.Feed, error) {
|
|
||||||
var (
|
|
||||||
posts []database.Post
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
rows, err := db.Query("SELECT * FROM posts")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var post database.Post
|
|
||||||
|
|
||||||
if err = post.Load(rows); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Public == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
posts = append(posts, post)
|
|
||||||
}
|
|
||||||
rows.Close()
|
|
||||||
|
|
||||||
blogurl, err := url.JoinPath(
|
|
||||||
config.Get("frontend_url"), "/blog",
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create the blog URL: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
feed := &feeds.Feed{
|
|
||||||
Title: "[ngn.tf] | blog",
|
|
||||||
Link: &feeds.Link{Href: blogurl},
|
|
||||||
Description: "ngn's personal blog",
|
|
||||||
Author: &feeds.Author{Name: "ngn", Email: "ngn@ngn.tf"},
|
|
||||||
Created: time.Now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Items = []*feeds.Item{}
|
|
||||||
for _, p := range posts {
|
|
||||||
purl, err := url.JoinPath(blogurl, p.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create URL for '%s': %s\n", p.ID, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := time.Parse("02/01/06", p.Date)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse time for '%s': %s\n", p.ID, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Items = append(feed.Items, &feeds.Item{
|
|
||||||
Id: p.ID,
|
|
||||||
Title: p.Title,
|
|
||||||
Link: &feeds.Link{Href: purl},
|
|
||||||
Author: &feeds.Author{Name: p.Author},
|
|
||||||
Created: parsed,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return feed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_Feed(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
db *sql.DB
|
|
||||||
err error
|
|
||||||
feed *feeds.Feed
|
|
||||||
name []string
|
|
||||||
res string
|
|
||||||
ext string
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
|
|
||||||
if name = strings.Split(path.Base(c.Path()), "."); len(name) != 2 {
|
|
||||||
return util.ErrNotFound(c)
|
|
||||||
}
|
|
||||||
ext = name[1]
|
|
||||||
|
|
||||||
if feed, err = getFeed(db); err != nil {
|
|
||||||
util.Fail("cannot obtain the feed: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ext {
|
|
||||||
case "atom":
|
|
||||||
res, err = feed.ToAtom()
|
|
||||||
c.Set("Content-Type", "application/atom+xml")
|
|
||||||
break
|
|
||||||
|
|
||||||
case "json":
|
|
||||||
res, err = feed.ToJSON()
|
|
||||||
c.Set("Content-Type", "application/feed+json")
|
|
||||||
break
|
|
||||||
|
|
||||||
case "rss":
|
|
||||||
res, err = feed.ToRss()
|
|
||||||
c.Set("Content-Type", "application/rss+xml")
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
return util.ErrNotFound(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
util.Fail("cannot obtain the feed as the specified format: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Send([]byte(res))
|
|
||||||
}
|
|
14
api/routes/index.go
Normal file
14
api/routes/index.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GET_Index(c *fiber.Ctx) error {
|
||||||
|
conf := c.Locals("config").(*config.Type)
|
||||||
|
app := conf.GetURL("app_url")
|
||||||
|
|
||||||
|
// redirect to the API documentation
|
||||||
|
return c.Redirect(app.JoinPath("/doc/api").String())
|
||||||
|
}
|
67
api/routes/metrics.go
Normal file
67
api/routes/metrics.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const VISITOR_CACHE_MAX = 30 // store 30 visitor data at most
|
||||||
|
var visitor_cache []string // in memory cache for the visitor addresses
|
||||||
|
|
||||||
|
func GET_Metrics(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
result map[string]uint64 = map[string]uint64{
|
||||||
|
"total": 0, // total number of visitors
|
||||||
|
"since": 0, // metric collection start date (UNIX timestamp)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
new_addr := util.GetSHA1(util.IP(c))
|
||||||
|
is_in_cache := false
|
||||||
|
|
||||||
|
for _, cache := range visitor_cache {
|
||||||
|
if new_addr == cache {
|
||||||
|
is_in_cache = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result["total"], err = db.MetricsGet("visitor_count"); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_in_cache {
|
||||||
|
if len(visitor_cache) > VISITOR_CACHE_MAX {
|
||||||
|
util.Debg("visitor cache is full, removing the oldest entry")
|
||||||
|
visitor_cache = visitor_cache[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
visitor_cache = append(visitor_cache, new_addr)
|
||||||
|
result["total"]++
|
||||||
|
|
||||||
|
if err = db.MetricsSet("visitor_count", result["total"]); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result["since"], err = db.MetricsGet("start_date"); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result["since"] == 0 {
|
||||||
|
result["since"] = uint64(time.Now().Truncate(24 * time.Hour).Unix())
|
||||||
|
|
||||||
|
if err = db.MetricsSet("start_date", result["since"]); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, fiber.Map{
|
||||||
|
"result": result,
|
||||||
|
})
|
||||||
|
}
|
78
api/routes/news.go
Normal file
78
api/routes/news.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// feed_entry is a temporary struct used to pass the news to the news.xml
|
||||||
|
type feed_entry struct {
|
||||||
|
Title string
|
||||||
|
Author string
|
||||||
|
Time time.Time
|
||||||
|
RFC3339 string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert UNIX timestamp to RFC3339 (format used by atom feeds)
|
||||||
|
func (e *feed_entry) From(news *database.News, lang string) {
|
||||||
|
e.Title = news.Title.Get(lang)
|
||||||
|
e.Author = news.Author
|
||||||
|
e.Time = time.Unix(int64(news.Time), 0)
|
||||||
|
e.RFC3339 = e.Time.Format(time.RFC3339)
|
||||||
|
e.Content = news.Content.Get(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET_News(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
entries []feed_entry
|
||||||
|
news database.News
|
||||||
|
indx uint64
|
||||||
|
feed []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
conf := c.Locals("config").(*config.Type)
|
||||||
|
app := conf.GetURL("app_url")
|
||||||
|
lang := c.Params("lang")
|
||||||
|
|
||||||
|
if lang == "" || len(lang) != 2 {
|
||||||
|
return util.ErrBadReq(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
lang = strings.ToLower(lang)
|
||||||
|
indx = 0
|
||||||
|
|
||||||
|
for db.NewsNext(&news) {
|
||||||
|
if news.Supports(lang) {
|
||||||
|
entries = append(entries, feed_entry{})
|
||||||
|
entries[indx].From(&news, lang)
|
||||||
|
indx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
return entries[i].Time.Before(entries[j].Time)
|
||||||
|
})
|
||||||
|
|
||||||
|
if feed, err = util.Render("views/news.xml", fiber.Map{
|
||||||
|
"updated": time.Now().Format(time.RFC3339),
|
||||||
|
"entries": entries,
|
||||||
|
"lang": lang,
|
||||||
|
"app": app,
|
||||||
|
}); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("Content-Disposition", "attachment; filename=\"news.atom\"")
|
||||||
|
c.Set("Content-Type", "application/atom+xml; charset=utf-8")
|
||||||
|
|
||||||
|
return c.Send(feed)
|
||||||
|
}
|
24
api/routes/projects.go
Normal file
24
api/routes/projects.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GET_Projects(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
projects []database.Project
|
||||||
|
project database.Project
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
for db.ProjectNext(&project) {
|
||||||
|
projects = append(projects, project)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, fiber.Map{
|
||||||
|
"result": projects,
|
||||||
|
})
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
"github.com/ngn13/website/api/util"
|
"github.com/ngn13/website/api/util"
|
||||||
@ -11,32 +9,29 @@ import (
|
|||||||
func GET_Services(c *fiber.Ctx) error {
|
func GET_Services(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
services []database.Service
|
services []database.Service
|
||||||
rows *sql.Rows
|
service database.Service
|
||||||
db *sql.DB
|
|
||||||
err error
|
|
||||||
)
|
)
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
db := c.Locals("database").(*database.Type)
|
||||||
|
name := c.Query("name")
|
||||||
|
|
||||||
if rows, err = db.Query("SELECT * FROM services"); err != nil {
|
if name != "" {
|
||||||
util.Fail("cannot load services: %s", err.Error())
|
if s, err := db.ServiceFind(name); err != nil {
|
||||||
return util.ErrServer(c)
|
return util.ErrInternal(c, err)
|
||||||
}
|
} else if s != nil {
|
||||||
defer rows.Close()
|
return util.JSON(c, 200, fiber.Map{
|
||||||
|
"result": s,
|
||||||
for rows.Next() {
|
})
|
||||||
var service database.Service
|
|
||||||
|
|
||||||
if err = service.Load(rows); err != nil {
|
|
||||||
util.Fail("error while loading service: %s", err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return util.ErrNotExist(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
for db.ServiceNext(&service) {
|
||||||
services = append(services, service)
|
services = append(services, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return util.JSON(c, 200, fiber.Map{
|
||||||
"error": "",
|
|
||||||
"result": services,
|
"result": services,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,139 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/ngn13/website/api/database"
|
|
||||||
"github.com/ngn13/website/api/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getVoteHash(id string, ip string) string {
|
|
||||||
return util.GetSHA512(id + "_" + ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_VoteGet(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
db *sql.DB
|
|
||||||
id string
|
|
||||||
hash string
|
|
||||||
vote database.Vote
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
|
|
||||||
if id = c.Query("id"); id == "" {
|
|
||||||
return util.ErrBadData(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = getVoteHash(id, util.GetIP(c))
|
|
||||||
|
|
||||||
if found, err = vote.Get(db, hash); err != nil {
|
|
||||||
util.Fail("error while searchig for a vote (\"%s\"): %s", hash, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return util.ErrEntryNotExists(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if vote.IsUpvote {
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"result": "upvote",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"result": "downvote",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GET_VoteSet(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
db *sql.DB
|
|
||||||
id string
|
|
||||||
is_upvote bool
|
|
||||||
hash string
|
|
||||||
vote database.Vote
|
|
||||||
post database.Post
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
db = *(c.Locals("database").(**sql.DB))
|
|
||||||
id = c.Query("id")
|
|
||||||
|
|
||||||
if c.Query("to") == "" || id == "" {
|
|
||||||
return util.ErrBadData(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if found, err = post.Get(db, id); err != nil {
|
|
||||||
util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return util.ErrEntryNotExists(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
is_upvote = c.Query("to") == "upvote"
|
|
||||||
hash = getVoteHash(id, util.GetIP(c))
|
|
||||||
|
|
||||||
if found, err = vote.Get(db, hash); err != nil {
|
|
||||||
util.Fail("error while searching for a vote (\"%s\"): %s", hash, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if found {
|
|
||||||
if vote.IsUpvote == is_upvote {
|
|
||||||
return util.ErrEntryExists(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if vote.IsUpvote && !is_upvote {
|
|
||||||
post.Vote -= 2
|
|
||||||
}
|
|
||||||
|
|
||||||
if !vote.IsUpvote && is_upvote {
|
|
||||||
post.Vote += 2
|
|
||||||
}
|
|
||||||
|
|
||||||
vote.IsUpvote = is_upvote
|
|
||||||
|
|
||||||
if err = post.Update(db); err != nil {
|
|
||||||
util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = vote.Update(db); err != nil {
|
|
||||||
util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.NoError(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
vote.Hash = hash
|
|
||||||
vote.IsUpvote = is_upvote
|
|
||||||
|
|
||||||
if is_upvote {
|
|
||||||
post.Vote++
|
|
||||||
} else {
|
|
||||||
post.Vote--
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = post.Update(db); err != nil {
|
|
||||||
util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = vote.Save(db); err != nil {
|
|
||||||
util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error())
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.NoError(c)
|
|
||||||
}
|
|
4
api/sql/admin_log.sql
Normal file
4
api/sql/admin_log.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS admin_log(
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
time INTEGER NOT NULL
|
||||||
|
);
|
4
api/sql/metrics.sql
Normal file
4
api/sql/metrics.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS metrics(
|
||||||
|
key TEXT NOT NULL UNIQUE,
|
||||||
|
value INTEGER NOT NULL
|
||||||
|
);
|
7
api/sql/news.sql
Normal file
7
api/sql/news.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS news(
|
||||||
|
id TEXT NOT NULL UNIQUE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
author TEXT NOT NULL,
|
||||||
|
time INTEGER NOT NULL,
|
||||||
|
content TEXT NOT NULL
|
||||||
|
);
|
6
api/sql/projects.sql
Normal file
6
api/sql/projects.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS projects(
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
desc TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
license TEXT
|
||||||
|
);
|
10
api/sql/services.sql
Normal file
10
api/sql/services.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS services(
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
desc TEXT NOT NULL,
|
||||||
|
check_time INTEGER NOT NULL,
|
||||||
|
check_res INTEGER NOT NULL,
|
||||||
|
check_url TEXT NOT NULL,
|
||||||
|
clear TEXT,
|
||||||
|
onion TEXT,
|
||||||
|
i2p TEXT
|
||||||
|
);
|
105
api/status/service.go
Normal file
105
api/status/service.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptrace"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
STATUS_RES_DOWN = 0 // service is down
|
||||||
|
STATUS_RES_OK = 1 // service is up
|
||||||
|
STATUS_RES_SLOW = 2 // service is up, but slow
|
||||||
|
STATUS_RES_NONE = 3 // service doesn't support status checking/status checking is disabled
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Type) check_http_service(service *database.Service) (r uint8, err error) {
|
||||||
|
var (
|
||||||
|
req *http.Request
|
||||||
|
res *http.Response
|
||||||
|
|
||||||
|
start time.Time
|
||||||
|
elapsed time.Duration
|
||||||
|
)
|
||||||
|
|
||||||
|
r = STATUS_RES_NONE
|
||||||
|
|
||||||
|
if req, err = http.NewRequest("GET", service.CheckURL, nil); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trace := &httptrace.ClientTrace{
|
||||||
|
GetConn: func(_ string) { start = time.Now() },
|
||||||
|
GotFirstResponseByte: func() { elapsed = time.Since(start) },
|
||||||
|
}
|
||||||
|
|
||||||
|
http.DefaultClient.Timeout = s.timeout
|
||||||
|
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
|
||||||
|
res, err = http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
if res != nil {
|
||||||
|
defer res.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.Debg("marking service \"%s\" as down (%s)", service.Name, err.Error())
|
||||||
|
err = nil
|
||||||
|
r = STATUS_RES_DOWN
|
||||||
|
} else if res.StatusCode != 200 {
|
||||||
|
util.Debg("marking service \"%s\" as down (status code %d)", service.Name, res.StatusCode)
|
||||||
|
r = STATUS_RES_DOWN
|
||||||
|
} else if elapsed.Microseconds() > s.limit.Microseconds() {
|
||||||
|
r = STATUS_RES_SLOW
|
||||||
|
} else {
|
||||||
|
r = STATUS_RES_OK
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) check_service(service *database.Service) error {
|
||||||
|
var (
|
||||||
|
res uint8
|
||||||
|
url *url.URL
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.disabled || service.CheckURL == "" {
|
||||||
|
err = nil
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if url, err = url.Parse(service.CheckURL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch url.Scheme {
|
||||||
|
case "https":
|
||||||
|
if res, err = s.check_http_service(service); err != nil {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
case "http":
|
||||||
|
if res, err = s.check_http_service(service); err != nil {
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// unsupported protocol
|
||||||
|
err = nil
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
service.CheckTime = uint64(time.Now().Unix())
|
||||||
|
service.CheckRes = res
|
||||||
|
return nil
|
||||||
|
|
||||||
|
fail:
|
||||||
|
service.CheckTime = 0
|
||||||
|
service.CheckRes = STATUS_RES_NONE
|
||||||
|
return err
|
||||||
|
}
|
139
api/status/status.go
Normal file
139
api/status/status.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type struct {
|
||||||
|
conf *config.Type
|
||||||
|
db *database.Type
|
||||||
|
|
||||||
|
ticker *time.Ticker
|
||||||
|
updateChan chan int
|
||||||
|
closeChan chan int
|
||||||
|
|
||||||
|
disabled bool
|
||||||
|
timeout time.Duration
|
||||||
|
limit time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) check() {
|
||||||
|
var (
|
||||||
|
services []database.Service
|
||||||
|
service database.Service
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
for s.db.ServiceNext(&service) {
|
||||||
|
services = append(services, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range services {
|
||||||
|
if err = s.check_service(&services[i]); err != nil {
|
||||||
|
util.Fail("failed to check the service status for \"%s\": %s", services[i].Name, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.db.ServiceUpdate(&services[i]); err != nil {
|
||||||
|
util.Fail("failed to update service status for \"%s\": %s", services[i].Name, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) loop() {
|
||||||
|
s.check()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
close(s.updateChan)
|
||||||
|
s.ticker.Stop()
|
||||||
|
s.closeChan <- 0
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-s.updateChan:
|
||||||
|
s.check()
|
||||||
|
|
||||||
|
case <-s.ticker.C:
|
||||||
|
s.check()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) Setup(conf *config.Type, db *database.Type) error {
|
||||||
|
var (
|
||||||
|
dur time.Duration
|
||||||
|
iv, to, lm string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
iv = conf.GetStr("interval")
|
||||||
|
to = conf.GetStr("timeout")
|
||||||
|
lm = conf.GetStr("limit")
|
||||||
|
|
||||||
|
if iv == "" || to == "" || lm == "" {
|
||||||
|
s.disabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if dur, err = util.GetDuration(iv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.timeout, err = util.GetDuration(iv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.limit, err = util.GetDuration(iv); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.conf = conf
|
||||||
|
s.db = db
|
||||||
|
|
||||||
|
s.ticker = time.NewTicker(dur)
|
||||||
|
s.updateChan = make(chan int)
|
||||||
|
s.closeChan = make(chan int)
|
||||||
|
|
||||||
|
s.disabled = false
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) Run() error {
|
||||||
|
if s.ticker == nil || s.updateChan == nil || s.closeChan == nil {
|
||||||
|
return fmt.Errorf("you either didn't call Setup() or you called it and it failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.disabled {
|
||||||
|
go s.check()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.loop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) Check() {
|
||||||
|
if !s.disabled {
|
||||||
|
s.updateChan <- 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Type) Stop() {
|
||||||
|
// tell loop() to stop
|
||||||
|
s.closeChan <- 0
|
||||||
|
|
||||||
|
// wait till loop() stops
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.closeChan:
|
||||||
|
close(s.closeChan)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
Info = log.New(os.Stdout, "\033[34m[info]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
COLOR_BLUE = "\033[34m"
|
||||||
Warn = log.New(os.Stderr, "\033[33m[warn]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
COLOR_YELLOW = "\033[33m"
|
||||||
Fail = log.New(os.Stderr, "\033[31m[fail]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
COLOR_RED = "\033[31m"
|
||||||
|
COLOR_CYAN = "\033[36m"
|
||||||
|
COLOR_RESET = "\033[0m"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Debg = log.New(os.Stdout, COLOR_CYAN+"[debg]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf
|
||||||
|
Info = log.New(os.Stdout, COLOR_BLUE+"[info]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf
|
||||||
|
Warn = log.New(os.Stderr, COLOR_YELLOW+"[warn]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf
|
||||||
|
Fail = log.New(os.Stderr, COLOR_RED+"[fail]"+COLOR_RESET+" ", log.Ltime|log.Lshortfile).Printf
|
||||||
)
|
)
|
||||||
|
79
api/util/res.go
Normal file
79
api/util/res.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IP(c *fiber.Ctx) string {
|
||||||
|
conf := c.Locals("config").(*config.Type)
|
||||||
|
ip_header := conf.GetStr("ip_header")
|
||||||
|
|
||||||
|
if ip_header != "" && c.Get(ip_header) != "" {
|
||||||
|
return strings.Clone(c.Get(ip_header))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.IP()
|
||||||
|
}
|
||||||
|
|
||||||
|
func JSON(c *fiber.Ctx, code int, data fiber.Map) error {
|
||||||
|
if data == nil {
|
||||||
|
data = fiber.Map{}
|
||||||
|
data["error"] = ""
|
||||||
|
} else if _, ok := data["error"]; !ok {
|
||||||
|
data["error"] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if data["error"] == 200 {
|
||||||
|
Warn("200 response with an error at %s", c.Path())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(code).JSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrInternal(c *fiber.Ctx, err error) error {
|
||||||
|
Warn("Internal server error at %s: %s", c.Path(), err.Error())
|
||||||
|
|
||||||
|
return JSON(c, http.StatusInternalServerError, fiber.Map{
|
||||||
|
"error": "Server error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrExists(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusConflict, fiber.Map{
|
||||||
|
"error": "Entry already exists",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrNotExist(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusNotFound, fiber.Map{
|
||||||
|
"error": "Entry does not exist",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrBadReq(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusBadRequest, fiber.Map{
|
||||||
|
"error": "Provided data is invalid",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrNotFound(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusNotFound, fiber.Map{
|
||||||
|
"error": "Endpoint not found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrBadJSON(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusBadRequest, fiber.Map{
|
||||||
|
"error": "Invalid JSON data",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrAuth(c *fiber.Ctx) error {
|
||||||
|
return JSON(c, http.StatusUnauthorized, fiber.Map{
|
||||||
|
"error": "Authentication failed",
|
||||||
|
})
|
||||||
|
}
|
67
api/util/util.go
Normal file
67
api/util/util.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Render(file string, data interface{}) ([]byte, error) {
|
||||||
|
var (
|
||||||
|
rendered *bytes.Buffer
|
||||||
|
tmpl *template.Template
|
||||||
|
content []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if content, err = os.ReadFile(file); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpl, err = template.New("template").Parse(string(content)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered = bytes.NewBuffer(nil)
|
||||||
|
err = tmpl.Execute(rendered, data)
|
||||||
|
|
||||||
|
return rendered.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDuration(d string) (time.Duration, error) {
|
||||||
|
var (
|
||||||
|
d_num uint64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
d_num_end := d[len(d)-1]
|
||||||
|
d_num_str := strings.TrimSuffix(d, string(d_num_end))
|
||||||
|
|
||||||
|
if d_num, err = strconv.ParseUint(d_num_str, 10, 64); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch d_num_end {
|
||||||
|
case 's':
|
||||||
|
return time.Duration(d_num) * (time.Second), nil
|
||||||
|
|
||||||
|
case 'm':
|
||||||
|
return time.Duration(d_num) * (time.Second * 60), nil
|
||||||
|
|
||||||
|
case 'h':
|
||||||
|
return time.Duration(d_num) * ((time.Second * 60) * 60), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("invalid time duration format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSHA1(s string) string {
|
||||||
|
hasher := sha1.New()
|
||||||
|
return hex.EncodeToString(hasher.Sum([]byte(s)))
|
||||||
|
}
|
@ -1,74 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha512"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetSHA512(s string) string {
|
|
||||||
hasher := sha512.New()
|
|
||||||
return fmt.Sprintf("%x", hasher.Sum([]byte(s)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TitleToID(name string) string {
|
|
||||||
return strings.ToLower(strings.ReplaceAll(name, " ", ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateToken() string {
|
|
||||||
s := make([]byte, 32)
|
|
||||||
for i := 0; i < 32; i++ {
|
|
||||||
s[i] = byte(65 + rand.Intn(25))
|
|
||||||
}
|
|
||||||
return string(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorJSON(error string) fiber.Map {
|
|
||||||
return fiber.Map{
|
|
||||||
"error": error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetIP(c *fiber.Ctx) string {
|
|
||||||
if c.Get("X-Real-IP") != "" {
|
|
||||||
return strings.Clone(c.Get("X-Real-IP"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.IP()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrServer(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusInternalServerError).JSON(ErrorJSON("Server error"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrEntryExists(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusConflict).JSON(ErrorJSON("Entry already exists"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrEntryNotExists(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusNotFound).JSON(ErrorJSON("Entry does not exist"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrBadData(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Provided data is invalid"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrBadJSON(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Bad JSON data"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrAuth(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusUnauthorized).JSON(ErrorJSON("Authentication failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrNotFound(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusNotFound).JSON(ErrorJSON("Requested endpoint not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func NoError(c *fiber.Ctx) error {
|
|
||||||
return c.Status(http.StatusOK).JSON(ErrorJSON(""))
|
|
||||||
}
|
|
16
api/views/news.xml
Normal file
16
api/views/news.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>{{.app.Host}} news</title>
|
||||||
|
<updated>{{.updated}}</updated>
|
||||||
|
<subtitle>News and updates about my projects and self-hosted services</subtitle>
|
||||||
|
<link href="{{.app.JoinPath "/news"}}"></link>
|
||||||
|
{{ range .entries }}
|
||||||
|
<entry>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<updated>{{.RFC3339}}</updated>
|
||||||
|
<author>
|
||||||
|
<name>{{.Author}}</name>
|
||||||
|
</author>
|
||||||
|
<content>{{.Content}}</content>
|
||||||
|
</entry>
|
||||||
|
{{ end }}
|
||||||
|
</feed>
|
@ -1,4 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.svelte-kit
|
.svelte-kit
|
||||||
build
|
|
||||||
public
|
public
|
||||||
|
build
|
||||||
|
9
app/.prettierrc
Normal file
9
app/.prettierrc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
@ -1,23 +1,39 @@
|
|||||||
FROM node:23.5.0 as build
|
# build the application with node
|
||||||
|
FROM node:23.5.0 AS build
|
||||||
|
|
||||||
|
ARG WEBSITE_REPORT_URL
|
||||||
|
ARG WEBSITE_SOURCE_URL
|
||||||
|
ARG WEBSITE_APP_URL
|
||||||
|
ARG WEBSITE_API_URL
|
||||||
|
ARG WEBSITE_DOC_URL
|
||||||
|
|
||||||
|
ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL
|
||||||
|
ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL
|
||||||
|
ENV WEBSITE_APP_URL=$WEBSITE_APP_URL
|
||||||
|
ENV WEBSITE_API_URL=$WEBSITE_API_URL
|
||||||
|
ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
ARG API_URL
|
|
||||||
ENV VITE_API_URL_DEV $API_URL
|
|
||||||
|
|
||||||
RUN npm install && npm run build
|
RUN npm install && npm run build
|
||||||
|
|
||||||
FROM oven/bun:1.1.20 as main
|
# run it with bun (a lot faster)
|
||||||
|
FROM oven/bun:latest AS main
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=build /app/build ./build
|
COPY --from=build /app/build ./build
|
||||||
COPY --from=build /app/package.json ./package.json
|
COPY --from=build /app/package.json ./package.json
|
||||||
COPY --from=build /app/package-lock.json ./package-lock.json
|
COPY --from=build /app/package-lock.json ./package-lock.json
|
||||||
|
|
||||||
EXPOSE 4173
|
RUN useradd runner -r -u 1001 -d /app
|
||||||
|
RUN chown -R runner:runner /app
|
||||||
|
|
||||||
|
USER runner
|
||||||
RUN bun install
|
RUN bun install
|
||||||
|
|
||||||
|
EXPOSE 7001
|
||||||
|
|
||||||
|
ENV PORT=7001
|
||||||
CMD ["bun", "build/index.js"]
|
CMD ["bun", "build/index.js"]
|
||||||
|
10
app/Makefile
Normal file
10
app/Makefile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
all:
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
format:
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
run:
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
.PHONY: format
|
3806
app/package-lock.json
generated
3806
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,24 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "5.0.0",
|
"version": "6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "VITE_API_URL_DEV=http://127.0.0.1:7001 vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview --host"
|
"preview": "vite preview",
|
||||||
},
|
"lint": "prettier --check .",
|
||||||
"devDependencies": {
|
"format": "prettier --write ."
|
||||||
"@sveltejs/adapter-auto": "^3.3.1",
|
},
|
||||||
"@sveltejs/adapter-node": "^5.2.11",
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "^2.15.1",
|
"@sveltejs/adapter-auto": "^3.3.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.3",
|
"@sveltejs/adapter-node": "^5.2.11",
|
||||||
"svelte": "^5.16.0",
|
"@sveltejs/kit": "^2.15.1",
|
||||||
"vite": "^5.4.11"
|
"@sveltejs/vite-plugin-svelte": "^4.0.3",
|
||||||
},
|
"prettier": "^3.4.2",
|
||||||
"type": "module",
|
"prettier-plugin-svelte": "^3.3.2",
|
||||||
"dependencies": {
|
"svelte": "^5.16.0",
|
||||||
"@types/dompurify": "^3.2.0",
|
"vite": "^5.4.11"
|
||||||
"dompurify": "^3.2.3",
|
},
|
||||||
"marked": "^15.0.4"
|
"type": "module",
|
||||||
}
|
"dependencies": {
|
||||||
|
"dompurify": "^3.2.3",
|
||||||
|
"marked": "^15.0.6",
|
||||||
|
"svelte-i18n": "^4.0.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=1024">
|
<meta name="viewport" content="width=1024" />
|
||||||
<link rel="icon" href="data:;base64,=">
|
<link rel="icon" href="data:;base64,=" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
37
app/src/lib/api.js
Normal file
37
app/src/lib/api.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { urljoin } from "$lib/util.js";
|
||||||
|
|
||||||
|
const api_version = "v1";
|
||||||
|
const api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version);
|
||||||
|
|
||||||
|
function api_urljoin(path = null, query = {}) {
|
||||||
|
return urljoin(api_url, path, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function api_check_err(json) {
|
||||||
|
if (!("error" in json)) throw new Error('API response is missing the "error" key');
|
||||||
|
|
||||||
|
if (json["error"] != "") throw new Error(`API returned an error: ${json["error"]}`);
|
||||||
|
|
||||||
|
if (!("result" in json)) throw new Error('API response is missing the "result" key');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api_http_get(fetch, url) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
const json = await res.json();
|
||||||
|
api_check_err(json);
|
||||||
|
return json["result"];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api_get_metrics(fetch) {
|
||||||
|
return await api_http_get(fetch, api_urljoin("/metrics"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api_get_services(fetch) {
|
||||||
|
return await api_http_get(fetch, api_urljoin("/services"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api_get_projects(fetch) {
|
||||||
|
return await api_http_get(fetch, api_urljoin("/projects"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { api_version, api_urljoin, api_get_metrics, api_get_services, api_get_projects };
|
@ -1,52 +1,50 @@
|
|||||||
<script>
|
<script>
|
||||||
export let title
|
export let title;
|
||||||
|
|
||||||
let current = ""
|
|
||||||
let i = 0
|
|
||||||
|
|
||||||
while (title.length > i) {
|
|
||||||
let c = title[i]
|
|
||||||
setTimeout(()=>{
|
|
||||||
current += c
|
|
||||||
}, 100*(i+1))
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="main">
|
<main>
|
||||||
<div class="title">
|
<h1 class="title">{title}</h1>
|
||||||
root@ngn.tf:~# {current}
|
<div>
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.main {
|
main {
|
||||||
display: flex;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-basis: 30%;
|
||||||
width: 100%;
|
|
||||||
background: var(--dark-three);
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: solid 1px var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
display: flex;
|
||||||
background: var(--dark-two);
|
flex-direction: column;
|
||||||
padding: 25px;
|
}
|
||||||
border-radius: 7px 7px 0px 0px;
|
|
||||||
font-size: 20px;
|
|
||||||
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
main .title {
|
||||||
background: var(--dark-three);
|
font-family:
|
||||||
padding: 30px;
|
Consolas,
|
||||||
color: white;
|
Monaco,
|
||||||
border-radius: 5px;
|
Lucida Console,
|
||||||
font-size: 25px;
|
Liberation Mono,
|
||||||
}
|
DejaVu Sans Mono,
|
||||||
|
Bitstream Vera Sans Mono,
|
||||||
|
Courier New,
|
||||||
|
monospace;
|
||||||
|
color: var(--white-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main .title::before {
|
||||||
|
content: "#";
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
color: var(--white-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
main div {
|
||||||
|
border-left: solid 1px var(--black-4);
|
||||||
|
padding: 25px 25px 10px 25px;
|
||||||
|
font-size: var(--size-4);
|
||||||
|
color: var(--white-1);
|
||||||
|
word-wrap: break-word;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 7px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let title
|
|
||||||
export let url
|
|
||||||
let audio
|
|
||||||
|
|
||||||
let current = ""
|
|
||||||
let i = 0
|
|
||||||
|
|
||||||
while (title.length > i) {
|
|
||||||
let c = title[i]
|
|
||||||
setTimeout(()=>{
|
|
||||||
current += c
|
|
||||||
}, 100*(i+1))
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function epicSound() {
|
|
||||||
audio.play()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<a on:click={epicSound} data-sveltekit-preload-data href={url}>
|
|
||||||
<audio bind:this={audio} preload="auto">
|
|
||||||
<source src="/click.wav" type="audio/mpeg" />
|
|
||||||
</audio>
|
|
||||||
<div class="title">
|
|
||||||
{current}
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--dark-three);
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: .4s;
|
|
||||||
text-decoration: none;
|
|
||||||
border: solid 1px var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover > .title {
|
|
||||||
text-shadow: var(--text-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
border: solid 1px var(--dark-two);
|
|
||||||
background: var(--dark-two);
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: 7px 7px 0px 0px;
|
|
||||||
font-size: 20px;
|
|
||||||
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
background: var(--dark-three);
|
|
||||||
padding: 30px;
|
|
||||||
padding-top: 30px;
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
</style>
|
|
28
app/src/lib/doc.js
Normal file
28
app/src/lib/doc.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { urljoin } from "$lib/util.js";
|
||||||
|
|
||||||
|
function doc_urljoin(path = null, query = {}) {
|
||||||
|
return urljoin(import.meta.env.WEBSITE_DOC_URL, path, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doc_check_err(json) {
|
||||||
|
if ("error" in json) throw new Error(`Documentation server returned an error: ${json["error"]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doc_http_get(fetch, url) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
const json = await res.json();
|
||||||
|
doc_check_err(json);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doc_get_list(fetch) {
|
||||||
|
return await doc_http_get(fetch, doc_urljoin("/list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doc_get(fetch, name) {
|
||||||
|
let url = doc_urljoin("/get");
|
||||||
|
url = urljoin(url, name);
|
||||||
|
return await doc_http_get(fetch, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { doc_urljoin, doc_get, doc_get_list };
|
62
app/src/lib/error.svelte
Normal file
62
app/src/lib/error.svelte
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<script>
|
||||||
|
import Link from "$lib/link.svelte";
|
||||||
|
import { color } from "$lib/util.js";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
export let error = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1 style="color: var(--{color()})">{$_("error.title")}</h1>
|
||||||
|
<code>
|
||||||
|
{#if error === ""}
|
||||||
|
Unknown error
|
||||||
|
{:else}
|
||||||
|
{error}
|
||||||
|
{/if}
|
||||||
|
</code>
|
||||||
|
<Link link={import.meta.env.WEBSITE_REPORT_URL}>
|
||||||
|
{$_("error.report")}
|
||||||
|
</Link>
|
||||||
|
<img src="/profile/sad.png" alt="" />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
padding: 50px;
|
||||||
|
font-size: var(--size-4);
|
||||||
|
|
||||||
|
background: var(--background);
|
||||||
|
background-size: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main h1 {
|
||||||
|
font-size: var(--size-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
main code {
|
||||||
|
font-size: var(--size-4);
|
||||||
|
color: var(--white-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
main img {
|
||||||
|
width: var(--profile-size);
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
89
app/src/lib/footer.svelte
Normal file
89
app/src/lib/footer.svelte
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<script>
|
||||||
|
import { urljoin, color, date_from_ts } from "$lib/util.js";
|
||||||
|
import { api_get_metrics } from "$lib/api.js";
|
||||||
|
import Link from "$lib/link.svelte";
|
||||||
|
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
let data = {};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
data = await api_get_metrics(fetch);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer style="border-top: solid 2px var(--{color()});">
|
||||||
|
<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>
|
||||||
|
{$_("footer.powered")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="useless">
|
||||||
|
<span>
|
||||||
|
{$_("footer.number", {
|
||||||
|
values: {
|
||||||
|
total: data.total,
|
||||||
|
since: date_from_ts(data.since),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
{#if data.number % 1000 == 0}
|
||||||
|
<span style="color: var(--{color()})">({$_("footer.wow")})</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{$_("footer.version", { values: { api_version: "v1", frontend_version: pkg.version } })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--black-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
color: var(--white-2);
|
||||||
|
font-size: var(--size-2);
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.useless {
|
||||||
|
margin: 25px 50px 25px 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin: 25px 0 25px 50px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info .links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
22
app/src/lib/head.svelte
Normal file
22
app/src/lib/head.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script>
|
||||||
|
import { api_urljoin } from "$lib/api.js";
|
||||||
|
import { app_url } from "$lib/util.js";
|
||||||
|
|
||||||
|
export let desc, title;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>[ngn.tf] | {title}</title>
|
||||||
|
|
||||||
|
<meta content="[ngn.tf] | {title}" property="og:title" />
|
||||||
|
<meta content={desc} property="og:description" />
|
||||||
|
<meta content={app_url()} property="og:url" />
|
||||||
|
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="alternate"
|
||||||
|
type="application/atom+xml"
|
||||||
|
href={api_urljoin("/news/en")}
|
||||||
|
title="Atom Feed"
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
@ -1,41 +1,80 @@
|
|||||||
<script>
|
<script>
|
||||||
export let subtitle = ""
|
import { color } from "$lib/util.js";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
export let picture = "";
|
||||||
|
export let title = "";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>
|
<div>
|
||||||
<slot></slot>
|
<h1 class="title" style="color: var(--{color()})">{title.toLowerCase()}</h1>
|
||||||
</h1>
|
<h1 class="cursor" style="color: var(--{color()})">_</h1>
|
||||||
<h4><c>{subtitle}</c></h4>
|
</div>
|
||||||
|
<img src="/profile/{picture}.png" alt="" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
header {
|
header {
|
||||||
background:
|
background: var(--background);
|
||||||
linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96)),
|
background-size: 50%;
|
||||||
url("https://files.ngn.tf/banner.png");
|
|
||||||
background-size: 50%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
width: 100%;
|
||||||
font-weight: 900;
|
height: 100%;
|
||||||
font-size: 500%;
|
display: flex;
|
||||||
padding: 120px;
|
flex-direction: row;
|
||||||
padding-bottom: 0;
|
justify-content: space-between;
|
||||||
text-align: center;
|
align-items: end;
|
||||||
color: white;
|
}
|
||||||
text-shadow: var(--text-shadow);
|
|
||||||
text-size-adjust: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
header div {
|
||||||
padding-bottom: 120px;
|
display: flex;
|
||||||
font-weight: 600;
|
flex-direction: row;
|
||||||
font-size: 200%;
|
align-items: end;
|
||||||
text-align: center;
|
padding: 50px 50px 30px 50px;
|
||||||
color: white;
|
font-size: var(--size-6);
|
||||||
text-size-adjust: 80%;
|
font-family:
|
||||||
}
|
Consolas,
|
||||||
|
Monaco,
|
||||||
|
Lucida Console,
|
||||||
|
Liberation Mono,
|
||||||
|
DejaVu Sans Mono,
|
||||||
|
Bitstream Vera Sans Mono,
|
||||||
|
Courier New,
|
||||||
|
monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
justify-content: start;
|
||||||
|
width: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
header div .title {
|
||||||
|
text-shadow: var(--text-shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
width: 0;
|
||||||
|
animation: typing 1s steps(20, end) forwards;
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
header div .cursor {
|
||||||
|
content: "_";
|
||||||
|
display: inline-block;
|
||||||
|
animation: blink 1.5s steps(2) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
header img {
|
||||||
|
padding: 50px 50px 0 50px;
|
||||||
|
width: var(--profile-size);
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 900px) {
|
||||||
|
header {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
header img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
6
app/src/lib/icon.svelte
Normal file
6
app/src/lib/icon.svelte
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
import { color } from "$lib/util.js";
|
||||||
|
export let icon = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<i style="color: var(--{color()});" class="nf {icon}"></i>
|
37
app/src/lib/link.svelte
Normal file
37
app/src/lib/link.svelte
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "$lib/icon.svelte";
|
||||||
|
import { color } from "$lib/util.js";
|
||||||
|
|
||||||
|
const default_color = "white-1";
|
||||||
|
|
||||||
|
export let active = false;
|
||||||
|
export let highlight = true;
|
||||||
|
export let link = "";
|
||||||
|
export let icon = "";
|
||||||
|
|
||||||
|
let style = "";
|
||||||
|
|
||||||
|
if (highlight) style = `text-decoration-color: var(--${color()});`;
|
||||||
|
|
||||||
|
if (active) style += `color: var(--${color()});`;
|
||||||
|
else style += `color: var(--${default_color});`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if icon != ""}
|
||||||
|
<Icon {icon} />
|
||||||
|
{/if}
|
||||||
|
{#if highlight}
|
||||||
|
<a data-sveltekit-preload-data {style} href={link}>
|
||||||
|
<slot></slot>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<a data-sveltekit-preload-data {style} class="no-highlight" href={link}>
|
||||||
|
<slot></slot>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.no-highlight:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
66
app/src/lib/locale.js
Normal file
66
app/src/lib/locale.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { init, locale, register, waitLocale } from "svelte-i18n";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { get, writable } from "svelte/store";
|
||||||
|
|
||||||
|
const locale_default = "en";
|
||||||
|
let locale_index = writable(0);
|
||||||
|
let locale_list = [];
|
||||||
|
|
||||||
|
function locale_setup() {
|
||||||
|
// english
|
||||||
|
register("en", () => import("../locales/en.json"));
|
||||||
|
locale_list.push({ code: "en", name: "English", icon: "🇬🇧" });
|
||||||
|
|
||||||
|
// turkish
|
||||||
|
register("tr", () => import("../locales/tr.json"));
|
||||||
|
locale_list.push({ code: "tr", name: "Turkish", icon: "🇹🇷" });
|
||||||
|
|
||||||
|
init({
|
||||||
|
fallbackLocale: locale_default,
|
||||||
|
initialLocale: get(locale),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function locale_from_browser() {
|
||||||
|
if (browser) return window.navigator.language.slice(0, 2).toLowerCase();
|
||||||
|
else return locale_default;
|
||||||
|
}
|
||||||
|
|
||||||
|
function locale_select(l = null) {
|
||||||
|
if (l === null) {
|
||||||
|
if (browser && null !== (l = localStorage.getItem("locale"))) locale_select(l);
|
||||||
|
else locale_select(locale_from_browser());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
l = l.slice(0, 2);
|
||||||
|
|
||||||
|
for (let i = 0; i < locale_list.length; i++) {
|
||||||
|
if (l !== locale_list[i].code) continue;
|
||||||
|
|
||||||
|
if (browser) localStorage.setItem("locale", l);
|
||||||
|
|
||||||
|
locale.set(l);
|
||||||
|
locale_index.set(i);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
locale.set(locale_default);
|
||||||
|
locale_index.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function locale_wait() {
|
||||||
|
await waitLocale();
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
locale,
|
||||||
|
locale_list,
|
||||||
|
locale_index,
|
||||||
|
locale_default,
|
||||||
|
locale_setup,
|
||||||
|
locale_wait,
|
||||||
|
locale_select,
|
||||||
|
locale_from_browser,
|
||||||
|
};
|
@ -1,51 +1,42 @@
|
|||||||
<script>
|
<script>
|
||||||
import NavbarLink from "./navbar_link.svelte";
|
import NavbarLink from "./navbar_link.svelte";
|
||||||
|
import NavbarSwitch from "./navbar_switch.svelte";
|
||||||
|
|
||||||
|
import { color } from "$lib/util.js";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<nav style="border-bottom: solid 2px var(--{color()});">
|
||||||
|
<h3 style="color: var(--{color()})">[ngn.tf]</h3>
|
||||||
<div>
|
<div>
|
||||||
<h3>[ngn.tf]</h3>
|
<NavbarLink link="/">{$_("navbar.home")}</NavbarLink>
|
||||||
</div>
|
<NavbarLink link="/services">{$_("navbar.services")}</NavbarLink>
|
||||||
|
<NavbarLink link="/donate">{$_("navbar.donate")}</NavbarLink>
|
||||||
<div>
|
<NavbarSwitch />
|
||||||
<NavbarLink link="/">home</NavbarLink>
|
|
||||||
<NavbarLink link="/services">services</NavbarLink>
|
|
||||||
<NavbarLink link="/blog">blog</NavbarLink>
|
|
||||||
<!-- <NavbarLink link="/donate">donate</NavbarLink> -->
|
|
||||||
<NavbarLink link="https://stats.ngn.tf">status</NavbarLink>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
nav {
|
nav {
|
||||||
background: var(--dark-one);
|
box-shadow: var(--box-shadow-1);
|
||||||
padding: 20px 26px 22px 20px;
|
background: var(--black-1);
|
||||||
display: flex;
|
padding: 20px 30px 20px 20px;
|
||||||
flex-direction: row;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
border-bottom: solid 1.5px black;
|
justify-content: space-between;
|
||||||
animation-name: borderAnimation;
|
}
|
||||||
animation-duration: 10s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
box-shadow: var(--def-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
font-size: 25px;
|
font-size: var(--size-4);
|
||||||
color: red;
|
}
|
||||||
animation-name: colorAnimation;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-duration: 10s;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -1,47 +1,27 @@
|
|||||||
<script>
|
<script>
|
||||||
import { page } from "$app/stores"
|
import { color, click } from "$lib/util.js";
|
||||||
export let link
|
import { page } from "$app/stores";
|
||||||
export let type
|
|
||||||
let audio
|
|
||||||
|
|
||||||
function epicSound() {
|
export let link;
|
||||||
audio.play()
|
|
||||||
|
function is_active() {
|
||||||
|
return $page.url.pathname == link;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<a
|
||||||
<audio bind:this={audio} preload="auto">
|
style="text-decoration-color: var(--{color()}); {is_active() ? `color: var(--${color()})` : ''}"
|
||||||
<source src="/click.wav" type="audio/mpeg" />
|
data-sveltekit-preload-data
|
||||||
</audio>
|
on:click={click}
|
||||||
{#if type==="icon"}
|
href={link}
|
||||||
<a class="icon" data-sveltekit-preload-data on:click={epicSound} href="{link}"><slot></slot></a>
|
>
|
||||||
{:else}
|
<slot></slot>
|
||||||
<a data-sveltekit-preload-data on:click={epicSound} href="{link}"><slot></slot></a>
|
</a>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
a {
|
a {
|
||||||
font-weight: 700;
|
font-weight: 900;
|
||||||
font-size: 22px;
|
font-size: var(--size-4);
|
||||||
text-decoration: none;
|
color: var(--white-1);
|
||||||
color: white;
|
}
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
text-shadow: 3px 4px 7px rgba(81, 67, 21, 0.8);
|
|
||||||
animation-name: underlineAnimation;
|
|
||||||
animation-duration: 5s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow: 3px 4px 7px rgba(81, 67, 21, 0.8);
|
|
||||||
animation-name: colorAnimation;
|
|
||||||
animation-duration: 5s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
37
app/src/lib/navbar_switch.svelte
Normal file
37
app/src/lib/navbar_switch.svelte
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
import { locale_list, locale_select, locale_index } from "$lib/locale.js";
|
||||||
|
|
||||||
|
let len = locale_list.length;
|
||||||
|
|
||||||
|
function get_next(indx) {
|
||||||
|
let new_indx = 0;
|
||||||
|
|
||||||
|
if (indx + 1 >= len) indx = 0;
|
||||||
|
else new_indx = indx + 1;
|
||||||
|
|
||||||
|
return locale_list[new_indx];
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
locale_select(get_next($locale_index).code);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click={next}>
|
||||||
|
{get_next($locale_index).icon}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
background: var(--black-2);
|
||||||
|
color: var(--white-1);
|
||||||
|
font-size: var(--size-4);
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: var(--black-1);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,78 +1,114 @@
|
|||||||
<script>
|
<script>
|
||||||
export let desc
|
import Icon from "$lib/icon.svelte";
|
||||||
export let url
|
import Link from "$lib/link.svelte";
|
||||||
|
|
||||||
let icon = "<i class='nf nf-md-clipboard_multiple'></i>"
|
import { color, time_from_ts } from "$lib/util.js";
|
||||||
let audio
|
import { locale, _ } from "svelte-i18n";
|
||||||
|
|
||||||
function copy() {
|
export let service = {};
|
||||||
audio.play()
|
|
||||||
navigator.clipboard.writeText(url)
|
|
||||||
icon = "<i class='nf nf-md-clipboard_check'></i>"
|
|
||||||
setTimeout(()=>{
|
|
||||||
icon = "<i class='nf nf-md-clipboard_multiple'></i>"
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<audio bind:this={audio} preload="auto">
|
<div class="info">
|
||||||
<source src="/click.wav" type="audio/mpeg" />
|
<div class="title">
|
||||||
</audio>
|
<h1>{service.name}</h1>
|
||||||
<div>
|
<p>{service.desc[$locale]}</p>
|
||||||
<h1><slot></slot></h1>
|
</div>
|
||||||
<p>{desc}</p>
|
<div class="links">
|
||||||
|
<Link highlight={false} link={service.clear}><Icon icon="nf-oct-link" /></Link>
|
||||||
|
{#if service.onion != ""}
|
||||||
|
<Link highlight={false} link={service.onion}><Icon icon="nf-linux-tor" /></Link>
|
||||||
|
{/if}
|
||||||
|
{#if service.i2p != ""}
|
||||||
|
<Link highlight={false} link={service.i2p}
|
||||||
|
><span style="color: var(--{color()})">I2P</span></Link
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="check">
|
||||||
<button on:click={copy}>{@html icon}</button>
|
<h1>
|
||||||
<a href="{url}"><i class="nf nf-oct-link_external"></i></a>
|
{$_("services.last", {
|
||||||
|
values: { time: time_from_ts(service.check_time) },
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
|
{#if service.check_res == 0}
|
||||||
|
<span style="background: var(--white-2)">
|
||||||
|
{$_("services.status.down")}
|
||||||
|
</span>
|
||||||
|
{:else if service.check_res == 1}
|
||||||
|
<span style="background: var(--{color()})">
|
||||||
|
{$_("services.status.up")}
|
||||||
|
</span>
|
||||||
|
{:else if service.check_res == 2}
|
||||||
|
<span style="background: var(--{color()}); filter: brightness(50%);">
|
||||||
|
{$_("services.status.slow")}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
padding: 30px 30px 30px 30px;
|
background: var(--black-3);
|
||||||
background: var(--dark-two);
|
border: solid 1px var(--black-4);
|
||||||
border-radius: var(--radius);
|
text-align: left;
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
border: solid 1px var(--border-color);
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
color: white;
|
|
||||||
gap: 100px;
|
|
||||||
transition: .4s;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex: 1 1 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div h1 {
|
flex: 1;
|
||||||
animation-name: colorAnimation;
|
flex-basis: 40%;
|
||||||
animation-duration: 10s;
|
}
|
||||||
animation-iteration-count: infinite;
|
|
||||||
font-size: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div p {
|
main .info {
|
||||||
margin-top: 10px;
|
padding: 25px;
|
||||||
font-size: 20px;
|
display: flex;
|
||||||
}
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--white-1);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
a, button {
|
main .info .title h1 {
|
||||||
text-align: center;
|
font-size: var(--size-5);
|
||||||
font-size: 30px;
|
margin-bottom: 8px;
|
||||||
text-decoration: none;
|
font-weight: 900;
|
||||||
color: white;
|
}
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover, button:hover{
|
main .info .title p {
|
||||||
animation-name: colorAnimation;
|
font-size: var(--size-4);
|
||||||
animation-duration: 5s;
|
color: var(--white-2);
|
||||||
animation-iteration-count: infinite;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main .info .links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: var(--size-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
main .check {
|
||||||
|
border-top: solid 1px var(--black-4);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--white-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main .check h1 {
|
||||||
|
padding: 15px 25px 15px 25px;
|
||||||
|
font-size: var(--size-4);
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .check span {
|
||||||
|
padding: 15px 25px 15px 25px;
|
||||||
|
font-size: var(--size-5);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--black-1);
|
||||||
|
font-weight: 1000;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
71
app/src/lib/util.js
Normal file
71
app/src/lib/util.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { locale_from_browser } from "$lib/locale.js";
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
"yellow",
|
||||||
|
"cyan",
|
||||||
|
"green",
|
||||||
|
"pinkish",
|
||||||
|
"red",
|
||||||
|
// "blue" (looks kinda ass)
|
||||||
|
];
|
||||||
|
|
||||||
|
let colors_pos = -1;
|
||||||
|
|
||||||
|
function color() {
|
||||||
|
if (colors_pos < 0) colors_pos = Math.floor(Math.random() * colors.length);
|
||||||
|
else if (colors_pos >= colors.length) colors_pos = 0;
|
||||||
|
|
||||||
|
return colors[colors_pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
function click() {
|
||||||
|
let audio = new Audio("/click.wav");
|
||||||
|
audio.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
function urljoin(url, path = null, query = {}) {
|
||||||
|
if (undefined === url || null === url) return;
|
||||||
|
|
||||||
|
let url_len = url.length;
|
||||||
|
|
||||||
|
if (url[url_len - 1] != "/") url += "/";
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (ts === 0 || ts === undefined) return;
|
||||||
|
|
||||||
|
let ts_date = new Date(ts * 1000);
|
||||||
|
let ts_zone = ts_date.toString().match(/([A-Z]+[\+-][0-9]+)/)[1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Intl.DateTimeFormat(locale_from_browser(), {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
}).format(ts_date) + ` (${ts_zone})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function date_from_ts(ts) {
|
||||||
|
if (ts === 0 || ts === undefined) return;
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(locale_from_browser(), {
|
||||||
|
month: "2-digit",
|
||||||
|
year: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).format(new Date(ts * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { color, click, urljoin, app_url, time_from_ts, date_from_ts };
|
83
app/src/locales/en.json
Normal file
83
app/src/locales/en.json
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"navbar": {
|
||||||
|
"home": "home",
|
||||||
|
"services": "services",
|
||||||
|
"donate": "donate"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "Hello world!",
|
||||||
|
"welcome": {
|
||||||
|
"title": "about",
|
||||||
|
"desc": "Welcome to my website, I'm ngn",
|
||||||
|
"whoami": "I'm a security, privacy and freedom advocate high-schooler from Turkey",
|
||||||
|
"interest": "I'm interested in system security and software development",
|
||||||
|
"support": "I love and support Free/Libre and Open Source Software (FLOSS)"
|
||||||
|
},
|
||||||
|
"work": {
|
||||||
|
"title": "work",
|
||||||
|
"desc": "I don't currently have a job, so I spend most of my time...",
|
||||||
|
"build": "building stupid shit",
|
||||||
|
"fix": "fixing stupid shit",
|
||||||
|
"ctf": "solving CTF challenges",
|
||||||
|
"contribute": "contributing to random projects",
|
||||||
|
"wiki": "expanding my wiki"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"title": "contact",
|
||||||
|
"desc": "Here are some useful links if you want to get in contact with me",
|
||||||
|
"prefer": "I highly prefer email, you can send encrypted emails using my PGP key"
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"title": "services",
|
||||||
|
"desc": "A part from working on stupid shit, I host free (as in freedom, and price) services available for all",
|
||||||
|
"speed": "All of these services are available over a 600 Mbit/s interface",
|
||||||
|
"security": "All use SSL encrypted connection and they are all privacy-respecting",
|
||||||
|
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
|
||||||
|
"bullshit": "No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit",
|
||||||
|
"link": "See all the services!"
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"title": "projects",
|
||||||
|
"desc": "I mostly work on free software projects, here are some of projects that you might find interesting"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"title": "Service Status",
|
||||||
|
"none": "No services found",
|
||||||
|
"search": "Search for a service",
|
||||||
|
"feed": "News and updates",
|
||||||
|
"last": "Last checked at {time}",
|
||||||
|
"status": {
|
||||||
|
"up": "Up",
|
||||||
|
"down": "Down",
|
||||||
|
"slow": "Slow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"donate": {
|
||||||
|
"title": "Donate Money!",
|
||||||
|
"info": "I spend a lot of time and money on different projects and maintaining different services.",
|
||||||
|
"price": "I mostly pay for hosting and electricity. Which when added up costs around 550₺ per month (~$15 at the time of writing).",
|
||||||
|
"details": "So even a small donation would be useful. And it would help me keep everything up and running.",
|
||||||
|
"thanks": "Also huge thanks to all of you who have donated so far!",
|
||||||
|
"table": {
|
||||||
|
"platform": "Platform",
|
||||||
|
"address": "Adress/Link"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"doc": {
|
||||||
|
"title": "Documentation"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"title": "Something went wrong!",
|
||||||
|
"report": "Report this issue"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"source": "Source",
|
||||||
|
"license": "License",
|
||||||
|
"privacy": "Privacy",
|
||||||
|
"powered": "Powered by Svelte, Go, SQLite and donations",
|
||||||
|
"number": "Visited {total} times since {since}",
|
||||||
|
"wow": "wow!!",
|
||||||
|
"version": "Using API version {api_version}, frontend version {frontend_version}"
|
||||||
|
}
|
||||||
|
}
|
84
app/src/locales/tr.json
Normal file
84
app/src/locales/tr.json
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"navbar": {
|
||||||
|
"home": "anasayfa",
|
||||||
|
"services": "servisler",
|
||||||
|
"donate": "bağış"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "Merhaba Dünya!",
|
||||||
|
"welcome": {
|
||||||
|
"title": "hakkımda",
|
||||||
|
"desc": "Websiteme hoşgeldiniz, ben ngn",
|
||||||
|
"whoami": "Türkiye'den, güvenlik, gizlik ve özgürlük savunucusu bir liseliyim",
|
||||||
|
"interest": "Sistem güvenliği ve yazılım geliştirmek ile ilgileniyorum",
|
||||||
|
"support": "Özgür/Libre ve Açık Kaynaklı Yazılımı (FLOSS) seviyorum ve destekliyorum"
|
||||||
|
},
|
||||||
|
"work": {
|
||||||
|
"title": "iş",
|
||||||
|
"desc": "Şuan bir işim yok, o yüzden zamanımın çoğunu şunlarla geçiriyorum:",
|
||||||
|
"build": "salak şeyler inşa etmek",
|
||||||
|
"fix": "salak şeyleri düzeltmek",
|
||||||
|
"ctf": "CTF challenge'ları çözmek",
|
||||||
|
"contribute": "rastgele projelere katkıda bulunmak",
|
||||||
|
"wiki": "wikimi genişletmek"
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"title": "iletişim",
|
||||||
|
"desc": "Eğer benim ile iletişime geçmek istiyorsanız, işte bazı faydalı linkler",
|
||||||
|
"prefer": "Email'i fazlasıyla tercih ediyorum, PGP anahtarım ile şifreli email'ler gönderebilirsiniz"
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"title": "servisler",
|
||||||
|
"desc": "Salak şeyler inşa etmenin yanı sıra, herkes için kullanıma açık özgür ve ücretsiz servisler host ediyorum",
|
||||||
|
"speed": "Tüm servisler 600 Mbit/s ağ arayüzü üzerinden erişilebilir",
|
||||||
|
"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",
|
||||||
|
"bullshit": "CDN yok, cloudflare yok, CAPTCHA yok, analitikler yok, boktan saçmalıklar yok",
|
||||||
|
"link": "Tüm servisleri incele!"
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"title": "projeler",
|
||||||
|
"desc": "Çoğunlukla özgür yazılım projeleri üzerinde çalışıyorum, işte ilginç bulabileceğiniz bazı projelerim"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"title": "Servis Durumu",
|
||||||
|
"none": "Servis bulunamadı",
|
||||||
|
"search": "Bir servisi ara",
|
||||||
|
"feed": "Yenilikler ve güncellemeler",
|
||||||
|
"last": "Son kontrol zamanı {time}",
|
||||||
|
"status": {
|
||||||
|
"up": "Çalışıyor",
|
||||||
|
"down": "Kapalı",
|
||||||
|
"slow": "Yavaş"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"donate": {
|
||||||
|
"title": "Para Bağışla!",
|
||||||
|
"info": "Farklı projeler ve farklı servisleri yönetmek için oldukça zaman ve para harcıyorum.",
|
||||||
|
"price": "Çoğunlukla hosting ve elektrik için ödeme yapıyorum. Bunlar eklendiği zaman aylık 550₺ civarı bir miktar oluyor (yazdığım sırada ~15$).",
|
||||||
|
"details": "Bu sebepten küçük bir bağış bile oldukça faydalı olacaktır. Ve herşeyi açık ve çalışmakta tutmama yardımcı olacaktır.",
|
||||||
|
"thanks": "Ayrıca şuana kadar bağışta bulunan herkese çok teşekkür ederim!",
|
||||||
|
"table": {
|
||||||
|
"platform": "Platform",
|
||||||
|
"address": "Adres/Bağlantı"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"doc": {
|
||||||
|
"title": "Dökümantasyon"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"title": "Birşeyler yanlış gitti!",
|
||||||
|
"report": "Bu sorunu raporlayın"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"source": "Kaynak",
|
||||||
|
"license": "Lisans",
|
||||||
|
"privacy": "Gizlilik",
|
||||||
|
"powered": "Svelte, Go, SQLite ve bağışlar tarafından destekleniyor",
|
||||||
|
"number": "{since} tarihinden beri {total} kez ziyaret edildi",
|
||||||
|
"wow": "vay be!!",
|
||||||
|
"version": "Kullan API versiyonu {api_version}, arayüz versiyonu {frontend_version}"
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte";
|
||||||
import { goto } from "$app/navigation"
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
onMount(()=>{
|
onMount(() => {
|
||||||
goto("/")
|
goto("/");
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
6
app/src/routes/+layout.js
Normal file
6
app/src/routes/+layout.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { locale_setup, locale_wait } from "$lib/locale.js";
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
locale_setup();
|
||||||
|
await locale_wait();
|
||||||
|
}
|
@ -1,12 +1,36 @@
|
|||||||
<script>
|
<script>
|
||||||
import Navbar from "../lib/navbar.svelte";
|
import Navbar from "$lib/navbar.svelte";
|
||||||
</script>
|
import Footer from "$lib/footer.svelte";
|
||||||
|
|
||||||
|
import { locale_select } from "$lib/locale.js";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
locale_select();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<slot></slot>
|
<div class="content">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@import "../../static/global.css";
|
@import "/global.css";
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
background: var(--black-1);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
15
app/src/routes/+page.js
Normal file
15
app/src/routes/+page.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { api_get_projects } from "$lib/api.js";
|
||||||
|
|
||||||
|
export async function load({ fetch }) {
|
||||||
|
try {
|
||||||
|
let projects = await api_get_projects(fetch);
|
||||||
|
return {
|
||||||
|
projects: null === projects ? [] : projects,
|
||||||
|
error: "",
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
error: err.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,172 +1,121 @@
|
|||||||
<script>
|
<script>
|
||||||
import Header from "../lib/header.svelte";
|
import Header from "$lib/header.svelte";
|
||||||
import Card from "../lib/card.svelte";
|
import Error from "$lib/error.svelte";
|
||||||
|
import Head from "$lib/head.svelte";
|
||||||
|
import Card from "$lib/card.svelte";
|
||||||
|
import Link from "$lib/link.svelte";
|
||||||
|
|
||||||
|
import { _, locale } from "svelte-i18n";
|
||||||
|
import { color } from "$lib/util.js";
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<Head title="home" desc="home page of my personal website" />
|
||||||
<title>[ngn.tf] | homepage</title>
|
<Header picture="tired" title={$_("home.title")} />
|
||||||
<meta content="[ngn] | homepage" property="og:title" />
|
|
||||||
<meta content="Homepage of my personal website" property="og:description" />
|
|
||||||
<meta content="https://ngn.tf" property="og:url" />
|
|
||||||
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
|
||||||
<link rel="alternate" type="application/atom+xml" href="{import.meta.env.VITE_API_URL_DEV+'/blog/feed.atom'}" title="Atom Feed">
|
|
||||||
<link rel="alternate" type="application/rss+xml" href="{import.meta.env.VITE_API_URL_DEV+'/blog/feed.rss'}" title="RSS Feed">
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<Header>
|
{#if data.error.length !== 0}
|
||||||
<c>echo</c>
|
<Error error={data.error} />
|
||||||
hello world!
|
{:else}
|
||||||
</Header>
|
<main>
|
||||||
|
<Card title={$_("home.welcome.title")}>
|
||||||
<main>
|
<span> 👋 {$_("home.welcome.desc")}</span>
|
||||||
<div class="flexbox">
|
|
||||||
<Card title="whoami">
|
|
||||||
<div class="whoami-box">
|
|
||||||
<div class="whoami-pic">
|
|
||||||
<img alt="My profile" src="https://files.ngn.tf/pplow.png">
|
|
||||||
<a href="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D">
|
|
||||||
<c><i class="nf nf-oct-key"></i> Keyoxide</c>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="whoami-text">
|
|
||||||
👋 Hello! I'm ngn!
|
|
||||||
<ul>
|
|
||||||
<li>🇹🇷 I'm a high school student from Turkey</li>
|
|
||||||
<li>🖥️ I'm interested in cyber security and programming.</li>
|
|
||||||
<li>❤️ I love and support Free/Libre and Open Source Software (FLOSS)</li>
|
|
||||||
<li>🐧 My GNU/Linux distribution of choice is Artix, however I am currently running Arch</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flexbox">
|
|
||||||
<Card title="ps -eaf">
|
|
||||||
I usually spend my time...
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><c>⌨️</c> building random projects</li>
|
<li>🇹🇷 {$_("home.welcome.whoami")}</li>
|
||||||
<li><c>👥</c> contributing stuff that I like</li>
|
<li>🖥️ {$_("home.welcome.interest")}</li>
|
||||||
<li><c>🚩</c> solving CTFs</li>
|
<li>❤️ {$_("home.welcome.support")}</li>
|
||||||
<li><c>🖥️</c> customizing my desktop</li>
|
|
||||||
<li><c>📑</c> posting random stuff on my blog, you should definitely check it out btw (it's very active)</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card title={$_("home.work.title")}>
|
||||||
<Card title="wall">
|
<span>{$_("home.work.desc")}</span>
|
||||||
Here are some links if you want to get in contact with me, I highly
|
|
||||||
prefer email and I usually respond to emails in 1 or 2 days, just make
|
|
||||||
sure to check your spam folder (turns out running a TOR relay gets your IP into multiple blacklists)
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>⌨️ {$_("home.work.build")}</li>
|
||||||
<c><i class="nf nf-cod-github"></i></c>
|
<li>🤦 {$_("home.work.fix")}</li>
|
||||||
<a href="https://github.com/ngn13">Github</a>
|
<li>🚩 {$_("home.work.ctf")}</li>
|
||||||
</li>
|
<li>👥 {$_("home.work.contribute")}</li>
|
||||||
<li>
|
<li>📑 {$_("home.work.wiki")}</li>
|
||||||
<c><i class="nf nf-md-mastodon"></i></c>
|
|
||||||
<a href="https://defcon.social/@ngn" rel="me">Mastodon</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<c><i class="nf nf-md-email"></i></c>
|
|
||||||
<a href="mailto:ngn@ngn.tf">Email</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<c><i class="nf nf-md-xmpp"></i></c>
|
|
||||||
<a href="xmpp:ngn@chat.ngn.tf">XMPP</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
<Card title={$_("home.links.title")}>
|
||||||
</main>
|
<span>{$_("home.links.desc")}:</span>
|
||||||
|
<ul>
|
||||||
<div class="version">
|
<li>
|
||||||
<p>v5.0</p>
|
<Link
|
||||||
</div>
|
icon="nf-fa-key"
|
||||||
|
link="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D"
|
||||||
|
>
|
||||||
|
PGP
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link icon="nf-md-email" link="mailto:ngn@ngn.tf">Email</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link icon="nf-md-mastodon" link="https://defcon.social/@ngn">Mastodon</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<span>
|
||||||
|
{$_("home.links.prefer")}
|
||||||
|
</span>
|
||||||
|
</Card>
|
||||||
|
<Card title={$_("home.services.title")}>
|
||||||
|
<span>
|
||||||
|
{$_("home.services.desc")}:
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<i style="color: var(--{color()});" class="nf nf-md-speedometer_slow"></i>
|
||||||
|
{$_("home.services.speed")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i style="color: var(--{color()});" class="nf nf-fa-lock"></i>
|
||||||
|
{$_("home.services.security")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i style="color: var(--{color()});" class="nf nf-fa-network_wired"></i>
|
||||||
|
{$_("home.services.privacy")}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i style="color: var(--{color()});" class="nf nf-md-eye_off"></i>
|
||||||
|
{$_("home.services.bullshit")}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Link link="/services">{$_("home.services.link")}</Link>
|
||||||
|
</Card>
|
||||||
|
<Card title={$_("home.projects.title")}>
|
||||||
|
<span>
|
||||||
|
{$_("home.projects.desc")}:
|
||||||
|
</span>
|
||||||
|
{#if data.error === undefined}
|
||||||
|
<ul>
|
||||||
|
{#each data.projects.filter((p) => {
|
||||||
|
return p.desc[$locale] !== "" && p.desc[$locale] !== null && p.desc[$locale] !== undefined;
|
||||||
|
}) as project}
|
||||||
|
<li>
|
||||||
|
<Link active={true} link={project.url}>{project.name}</Link>:
|
||||||
|
{project.desc[$locale]}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
</main>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main{
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-wrap: wrap;
|
||||||
gap: 28px;
|
justify-content: center;
|
||||||
padding: 50px;
|
align-items: stretch;
|
||||||
}
|
|
||||||
|
|
||||||
.flexbox {
|
padding: 50px;
|
||||||
display: flex;
|
gap: 28px;
|
||||||
gap: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.whoami-box {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.whoami-pic {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
|
|
||||||
border-right: solid 1px var(--dark-fife);
|
|
||||||
padding: 0 35px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.whoami-pic img {
|
|
||||||
width: 200px;
|
|
||||||
border-radius: 20px;
|
|
||||||
|
|
||||||
border: solid 1px var(--border-color);
|
|
||||||
animation-name: fullBorderAnimation;
|
|
||||||
animation-duration: 10s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
|
|
||||||
box-shadow: rgba(50, 50, 93, 1) 0px 30px 60px -12px inset, rgba(0, 0, 0, 1) 0px 18px 36px -18px inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.version {
|
|
||||||
color: var(--dark-fife);
|
|
||||||
position: fixed;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 1200px) {
|
|
||||||
.flexbox {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 900px) {
|
@media only screen and (max-width: 900px) {
|
||||||
.whoami-box {
|
main {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 25px;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.whoami-pic {
|
|
||||||
border-right: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
export async function load({ fetch }) {
|
|
||||||
const api = import.meta.env.VITE_API_URL_DEV
|
|
||||||
const res = await fetch(api+"/blog/sum")
|
|
||||||
const data = await res.json()
|
|
||||||
|
|
||||||
return {
|
|
||||||
posts: data["result"]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Header from "../../lib/header.svelte";
|
|
||||||
import CardLink from "../../lib/card_link.svelte";
|
|
||||||
|
|
||||||
export let data
|
|
||||||
let posts = data.posts
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>[ngn.tf] | blog</title>
|
|
||||||
<meta content="[ngn] | blog" property="og:title" />
|
|
||||||
<meta content="View my blog posts" property="og:description" />
|
|
||||||
<meta content="https://ngn.tf" property="og:url" />
|
|
||||||
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
|
||||||
<link rel="alternate" type="application/atom+xml" href="{import.meta.env.VITE_API_URL_DEV+'/blog/feed.atom'}" title="Atom Feed">
|
|
||||||
<link rel="alternate" type="application/rss+xml" href="{import.meta.env.VITE_API_URL_DEV+'/blog/feed.rss'}" title="RSS Feed">
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<Header>
|
|
||||||
<c>/dev/</c>blog
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<div class="feed-list">
|
|
||||||
<a href="{import.meta.env.VITE_API_URL_DEV+'/blog/feed.rss'}">
|
|
||||||
<c><i class="nf nf-fa-rss_square"></i></c> <p>RSS</p>
|
|
||||||
</a>
|
|
||||||
<a href="{import.meta.env.VITE_API_URL_DEV+'/blog/feed.atom'}">
|
|
||||||
<c><i class="nf nf-fae-atom"></i></c> <p>Atom</p>
|
|
||||||
</a>
|
|
||||||
<a href="{import.meta.env.VITE_API_URL_DEV+'/blog/feed.json'}">
|
|
||||||
<c><i class="nf nf-seti-json"></i></c> <p>JSON</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="post-list">
|
|
||||||
{#each posts as post}
|
|
||||||
<CardLink url="/blog/{post.id}" title="{post.title}">
|
|
||||||
<p>{post.author} | {post.date}</p>
|
|
||||||
<br>
|
|
||||||
{post.content}...
|
|
||||||
</CardLink>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.post-list{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 15%;
|
|
||||||
padding-top: 50px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feed-list{
|
|
||||||
text-align: right;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feed-list a {
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 10px 15px 10px 15px;
|
|
||||||
background: var(--dark-three);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: solid 1px var(--border-color);
|
|
||||||
color: var(--white);
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 900;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
|
|
||||||
transition: .2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feed-list a:hover {
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feed-list a i{
|
|
||||||
font-size: 21px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 1316px) {
|
|
||||||
main {
|
|
||||||
padding: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,14 +0,0 @@
|
|||||||
export async function load({ fetch, params }) {
|
|
||||||
const id = params.id
|
|
||||||
const api = import.meta.env.VITE_API_URL_DEV
|
|
||||||
const res = await fetch(api+"/blog/get?id="+id)
|
|
||||||
const data = await res.json()
|
|
||||||
|
|
||||||
if (data["error"] != "") {
|
|
||||||
return {
|
|
||||||
error: data["error"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data["result"]
|
|
||||||
}
|
|
@ -1,191 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Header from "../../../lib/header.svelte"
|
|
||||||
import { goto } from "$app/navigation"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import DOMPurify from "dompurify"
|
|
||||||
import { marked } from "marked"
|
|
||||||
|
|
||||||
export let data
|
|
||||||
let sanitized
|
|
||||||
const api = import.meta.env.VITE_API_URL_DEV
|
|
||||||
|
|
||||||
let upvote_status = "inactive"
|
|
||||||
let downvote_status = "inactive"
|
|
||||||
let voted = false
|
|
||||||
let audio
|
|
||||||
|
|
||||||
async function get_status() {
|
|
||||||
const res = await fetch(api+"/blog/vote/get?id="+data.id)
|
|
||||||
const json = await res.json()
|
|
||||||
|
|
||||||
if(json["error"]!= ""){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json["result"] == "upvote") {
|
|
||||||
upvote_status = "active"
|
|
||||||
downvote_status = "inactive"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
downvote_status = "active"
|
|
||||||
upvote_status = "inactive"
|
|
||||||
}
|
|
||||||
|
|
||||||
voted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async ()=>{
|
|
||||||
if (data.title == undefined)
|
|
||||||
goto("/blog")
|
|
||||||
|
|
||||||
sanitized = DOMPurify.sanitize(
|
|
||||||
marked.parse(data.content, { breaks: true }),
|
|
||||||
{
|
|
||||||
ADD_TAGS: ["iframe"],
|
|
||||||
ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "scrolling"]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await get_status()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function upvote(){
|
|
||||||
audio.play()
|
|
||||||
const res = await fetch(api+"/blog/vote/set?id="+data.id+"&to=upvote")
|
|
||||||
const json = await res.json()
|
|
||||||
|
|
||||||
if(json["error"] != ""){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (voted){
|
|
||||||
data.vote += 2
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
voted = true
|
|
||||||
data.vote += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
await get_status()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downvote(){
|
|
||||||
audio.play()
|
|
||||||
const res = await fetch(api+"/blog/vote/set?id="+data.id+"&to=downvote")
|
|
||||||
const json = await res.json()
|
|
||||||
|
|
||||||
if(json["error"] != ""){
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (voted){
|
|
||||||
data.vote -= 2
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
voted = true
|
|
||||||
data.vote -= 1
|
|
||||||
}
|
|
||||||
|
|
||||||
await get_status()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>[ngn.tf] | {data.title}</title>
|
|
||||||
<meta content="[ngn] | blog" property="og:title" />
|
|
||||||
<meta content="{data.content.substring(0, 100)}..." property="og:description" />
|
|
||||||
<meta content="https://ngn.tf" property="og:url" />
|
|
||||||
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
|
||||||
<link href="/markdown.css" rel="stylesheet">
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<Header subtitle="{data.author} | {data.date}">
|
|
||||||
{data.title}
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<audio bind:this={audio} preload="auto">
|
|
||||||
<source src="/click.wav" type="audio/mpeg" />
|
|
||||||
</audio>
|
|
||||||
<div class="content markdown-body">
|
|
||||||
{@html sanitized}
|
|
||||||
</div>
|
|
||||||
<div class="votes">
|
|
||||||
<button on:click={async ()=>{upvote()}} class="{upvote_status}">
|
|
||||||
<i class="nf nf-md-arrow_up_bold"></i>
|
|
||||||
</button>
|
|
||||||
<p>{data.vote}</p>
|
|
||||||
<button on:click={async ()=>{downvote()}} class="{downvote_status}">
|
|
||||||
<i class="nf nf-md-arrow_down_bold"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
main {
|
|
||||||
padding: 50px 10% 50px 10%;
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 816px) {
|
|
||||||
main {
|
|
||||||
padding: 50px 20% 50px 20%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 30px;
|
|
||||||
background: var(--dark-four);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: solid 1px var(--border-color);
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
width: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.votes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: var(--text-shadow);
|
|
||||||
gap: 10px;
|
|
||||||
padding: 15px 5px 15px 5px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.votes p {
|
|
||||||
font-size: 25px;
|
|
||||||
color: var(--dark-six);
|
|
||||||
}
|
|
||||||
|
|
||||||
.votes button{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 10px;
|
|
||||||
background: none;
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--dark-six);
|
|
||||||
}
|
|
||||||
|
|
||||||
.votes button:hover {
|
|
||||||
animation-name: colorAnimation;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-duration: 10s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
animation-name: colorAnimation;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-duration: 10s;
|
|
||||||
}
|
|
||||||
</style>
|
|
13
app/src/routes/doc/[name]/+page.server.js
Normal file
13
app/src/routes/doc/[name]/+page.server.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { doc_get_list, doc_get } from "$lib/doc";
|
||||||
|
|
||||||
|
export async function load({ fetch, params }) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
docs: await doc_get_list(fetch),
|
||||||
|
doc: await doc_get(fetch, params.name),
|
||||||
|
error: "",
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { error: err.toString() };
|
||||||
|
}
|
||||||
|
}
|
123
app/src/routes/doc/[name]/+page.svelte
Normal file
123
app/src/routes/doc/[name]/+page.svelte
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<script>
|
||||||
|
import Header from "$lib/header.svelte";
|
||||||
|
import Error from "$lib/error.svelte";
|
||||||
|
import Head from "$lib/head.svelte";
|
||||||
|
|
||||||
|
import { locale, _ } from "svelte-i18n";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { color } from "$lib/util.js";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
marked.use({ breaks: true });
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
for (let key in data.doc)
|
||||||
|
data.doc[key]["content"] = DOMPurify.sanitize(data.doc[key]["content"]);
|
||||||
|
|
||||||
|
if (undefined !== data.error && data.error.includes("not found")) goto("/");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Head title="documentation" desc="website and API documentation" />
|
||||||
|
<Header picture="reader" title={$_("doc.title")} />
|
||||||
|
|
||||||
|
{#if data.error.length !== 0}
|
||||||
|
{#if !data.error.includes("not found")}
|
||||||
|
<Error error={data.error} />
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<main>
|
||||||
|
{#if data.doc !== undefined}
|
||||||
|
<div class="markdown-body" style="--link-color: var(--{color()})">
|
||||||
|
{@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>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "/markdown.css";
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
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) {
|
||||||
|
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>
|
@ -1,88 +1,98 @@
|
|||||||
<script>
|
<script>
|
||||||
import Header from "../../lib/header.svelte";
|
import Header from "$lib/header.svelte";
|
||||||
import Card from "../../lib/card.svelte";
|
import Head from "$lib/head.svelte";
|
||||||
|
import Icon from "$lib/icon.svelte";
|
||||||
|
|
||||||
|
import { color } from "$lib/util.js";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<Head title="donate" desc="give me all of your life savings" />
|
||||||
<title>[ngn.tf] | donate</title>
|
<Header picture="money" title={$_("donate.title")} />
|
||||||
<meta content="[ngn] | donate" property="og:title" />
|
|
||||||
<meta content="Give me all of your life savings" property="og:description" />
|
|
||||||
<meta content="https://ngn.tf" property="og:url" />
|
|
||||||
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<Header>
|
|
||||||
<c>bash</c>
|
|
||||||
donate.sh
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Card title="bash donate.sh">
|
<span> </span>
|
||||||
I work on free/libre and open source software and offer free services. General hosting
|
<span>
|
||||||
and stuff costs around 550₺ (~$17) per month, so feel free to donate in order to help me keep
|
{$_("donate.info")}
|
||||||
everything up and running!
|
{$_("donate.price")}
|
||||||
<table>
|
</span>
|
||||||
<thead>
|
<br />
|
||||||
<tr>
|
<br />
|
||||||
<th>Platform</th>
|
<span>
|
||||||
<th>Address/Link</th>
|
{$_("donate.details")}
|
||||||
</tr>
|
</span>
|
||||||
</thead>
|
<table>
|
||||||
<tbody>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Monero (XMR)</td>
|
<th style="color: var(--{color()})">{$_("donate.table.platform")}</th>
|
||||||
<td>
|
<th style="color: var(--{color()})">{$_("donate.table.address")}</th>
|
||||||
<code>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Icon icon="nf-fa-monero" />
|
||||||
|
Monero (XMR)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>
|
||||||
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
|
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
|
||||||
</code>
|
</code>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
Also huge thanks to all of you who has donated so far, even if it's a small amount, I highly
|
<span>
|
||||||
appreciate it. Thank you!
|
{$_("donate.thanks")}
|
||||||
</Card>
|
</span>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main{
|
main {
|
||||||
display: flex;
|
padding: 50px;
|
||||||
flex-direction: column;
|
}
|
||||||
gap: 35px;
|
|
||||||
padding: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
main span {
|
||||||
border-collapse: collapse;
|
font-size: var(--size-4);
|
||||||
border: none;
|
color: var(--white-1);
|
||||||
color: white;
|
}
|
||||||
font-size: 20px;
|
|
||||||
width: 100%;
|
|
||||||
margin: 30px 0 30px 0;
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
table {
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
background: var(--black-3);
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: var(--size-3);
|
||||||
|
margin: 30px 0 30px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
tr,th,td{
|
tr,
|
||||||
color: white;
|
th,
|
||||||
background: var(--dark-two);
|
td {
|
||||||
}
|
color: white;
|
||||||
|
background: var(--dark-two);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
td,th{
|
td,
|
||||||
border: solid 1px var(--dark-fife);
|
th {
|
||||||
padding: 16px;
|
font-size: var(--size-4);
|
||||||
}
|
border: solid 1px var(--black-4);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
animation-name: colorAnimation;
|
font-weight: 1000;
|
||||||
animation-duration: 10s;
|
}
|
||||||
animation-iteration-count: infinite;
|
|
||||||
background: var(--dark-two);
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
td {
|
||||||
word-wrap: break-word;
|
color: var(--white-2);
|
||||||
white-space: pre-wrap;
|
font-weight: 400;
|
||||||
word-break: break-word;
|
}
|
||||||
}
|
|
||||||
|
code {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,43 +1,15 @@
|
|||||||
|
import { api_get_services } from "$lib/api.js";
|
||||||
|
|
||||||
export async function load({ fetch }) {
|
export async function load({ fetch }) {
|
||||||
const api = import.meta.env.VITE_API_URL_DEV
|
try {
|
||||||
const res = await fetch(api+"/services/all")
|
let services = await api_get_services(fetch);
|
||||||
const data = await res.json()
|
|
||||||
|
|
||||||
if (data["error"] != ""){
|
|
||||||
return {
|
return {
|
||||||
error: data["error"]
|
services: null === services ? [] : services,
|
||||||
}
|
error: "",
|
||||||
}
|
};
|
||||||
|
} catch (err) {
|
||||||
// Some really bad code to convert
|
return {
|
||||||
// [service1, service2, service3...]
|
error: err.toString(),
|
||||||
|
};
|
||||||
// to
|
|
||||||
|
|
||||||
// [[service1, service2], [service4, service5], [service4...]...]
|
|
||||||
// so i can render it in the UI easily
|
|
||||||
|
|
||||||
let all = data["result"]
|
|
||||||
let counter = 0
|
|
||||||
let currentlist = []
|
|
||||||
let services = []
|
|
||||||
|
|
||||||
for (let i = 0; i < all.length; i++){
|
|
||||||
currentlist.push(all[i])
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
if(i == all.length-1 && counter != 2){
|
|
||||||
services.push(currentlist)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counter == 2) {
|
|
||||||
services.push(currentlist)
|
|
||||||
currentlist = []
|
|
||||||
counter = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
services
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,100 +1,92 @@
|
|||||||
<script>
|
<script>
|
||||||
import Header from "../../lib/header.svelte";
|
import Service from "$lib/service.svelte";
|
||||||
import Service from "../../lib/service.svelte";
|
import Header from "$lib/header.svelte";
|
||||||
import Card from "../../lib/card.svelte";
|
import Error from "$lib/error.svelte";
|
||||||
|
import Link from "$lib/link.svelte";
|
||||||
|
import Head from "$lib/head.svelte";
|
||||||
|
|
||||||
export let data
|
import { api_urljoin } from "$lib/api.js";
|
||||||
|
import { locale, _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
let services = $state(data.services);
|
||||||
|
|
||||||
|
function change(input) {
|
||||||
|
let value = input.target.value.toLowerCase();
|
||||||
|
services = [];
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
services = data.services;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.services.forEach((s) => {
|
||||||
|
if (s.name.toLowerCase().includes(value)) services.push(s);
|
||||||
|
else if (s.desc[$locale].toLowerCase().includes(value)) services.push(s);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_services() {
|
||||||
|
return services.filter((s) => {
|
||||||
|
return s.desc[$locale] !== "" && s.desc[$locale] !== null && s.desc[$locale] !== undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<Head title="services" desc="my self-hosted services and projects" />
|
||||||
<title>[ngn.tf] | services</title>
|
<Header picture="cool" title={$_("services.title")} />
|
||||||
<meta content="[ngn] | services" property="og:title" />
|
|
||||||
<meta content="Stuff that I host" property="og:description" />
|
{#if data.error.length !== 0}
|
||||||
<meta content="https://ngn.tf" property="og:url" />
|
<Error error={data.error} />
|
||||||
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
{:else}
|
||||||
</svelte:head>
|
<main>
|
||||||
<Header><c>ls</c> services</Header>
|
<div class="title">
|
||||||
<main>
|
<input oninput={change} type="text" placeholder={$_("services.search")} />
|
||||||
<Card title="cat services/*/info.txt">
|
<div>
|
||||||
<div class="flexcol">
|
<Link icon="nf-fa-feed" link={api_urljoin("/news/" + $locale)}>{$_("services.feed")}</Link>
|
||||||
{#each data.services as services_list}
|
|
||||||
<div class="flexrow">
|
|
||||||
{#each services_list as service}
|
|
||||||
<Service url="{service.url}" desc="{service.desc}">{service.name}</Service>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
<div class="services">
|
||||||
</Card>
|
{#if get_services().length == 0}
|
||||||
|
<h3 class="none">{$_("services.none")}</h3>
|
||||||
<Card title="cat services/details.txt">
|
{:else}
|
||||||
Here some details for all the services I offer:
|
{#each get_services() as service}
|
||||||
<ul>
|
<Service {service} />
|
||||||
<li>
|
{/each}
|
||||||
<c><i class="nf nf-cod-account"></i> Registration:</c> All the services are offered for free, and all of them
|
{/if}
|
||||||
are accessiable to public. And registrations are open for the all services that support account registrations.
|
</div>
|
||||||
</li>
|
</main>
|
||||||
<li>
|
{/if}
|
||||||
<c><i class="nf nf-fa-eye_slash"></i> Privacy:</c> To protect user privacy, all the web proxy logs are cleared regularly.
|
|
||||||
I also do not use any kind of CDN, and provide SSL encrypted connection for all the services. You can also connect all the
|
|
||||||
services over TOR, as I do not block any traffic from TOR.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<c><i class="nf nf-oct-graph"></i> Uptime:</c> Some services get restarted regularly, also sometimes I have
|
|
||||||
issues with the hosting, so I cannot provide or guarantee %100 uptime for any of the services. I also cannot guarantee
|
|
||||||
the that there won't be any data loss. I do take any regular backups, but your data may be lost in case of something
|
|
||||||
like a SSD failure.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<c><i class="nf nf-md-speedometer"></i> Speed:</c> All the services are located in Turkey, and avaliable
|
|
||||||
over an 400 Mbit/s interface. If you are close to Turkey you should have a fairly good experience.
|
|
||||||
If you are not, then you should probably use another provider for the service.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
display: flex;
|
padding: 50px;
|
||||||
flex-direction: column;
|
text-align: right;
|
||||||
align-content: center;
|
}
|
||||||
justify-content: center;
|
|
||||||
padding: 50px;
|
main .title {
|
||||||
gap: 28px;
|
display: flex;
|
||||||
}
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
.flexcol {
|
align-items: center;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
|
||||||
align-content: center;
|
main .none {
|
||||||
justify-content: center;
|
color: var(--white-3);
|
||||||
gap: 13px;
|
}
|
||||||
}
|
|
||||||
|
main .services {
|
||||||
.flexrow {
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-wrap: wrap;
|
||||||
align-content: center;
|
justify-content: center;
|
||||||
justify-content: center;
|
align-items: stretch;
|
||||||
width: 100%;
|
gap: 28px;
|
||||||
gap: 13px;
|
}
|
||||||
}
|
|
||||||
|
@media only screen and (max-width: 1200px) {
|
||||||
ul {
|
main .services {
|
||||||
list-style: inside;
|
flex-direction: column;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding-top: 30px;
|
|
||||||
line-height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 1316px) {
|
|
||||||
.flexrow {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
21
app/static/animations.css
Normal file
21
app/static/animations.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@keyframes blink {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cursor {
|
||||||
|
to {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
from {
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
BIN
app/static/banner.png
Normal file
BIN
app/static/banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
@ -1,23 +1,38 @@
|
|||||||
|
@import "./animations.css";
|
||||||
@import "./font.css";
|
@import "./font.css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--white: white;
|
--yellow: #d3b910;
|
||||||
--dark-one: black;
|
--cyan: #0dd2e8;
|
||||||
--dark-two: #050505;
|
--green: #06e00a;
|
||||||
--dark-three: #121212;
|
--pinkish: #d506e0;
|
||||||
--dark-four: #101010;
|
--red: #e8180d;
|
||||||
--dark-fife: #3a3b3c;
|
--blue: #2036f9;
|
||||||
--dark-six: #C0C0C0;
|
|
||||||
--radius: 8px;
|
--white-1: #ffffff;
|
||||||
/*
|
--white-2: #bfbfbf;
|
||||||
old shadow animation
|
--white-3: #5f5f5f;
|
||||||
--def-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px,
|
--white-4: #0f0f0f;
|
||||||
rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px,
|
|
||||||
rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px;
|
--black-1: #000000;
|
||||||
*/
|
--black-2: #050505;
|
||||||
--text-shadow: 0px 10px 20px rgba(90, 90, 90, 0.8);
|
--black-3: #111111;
|
||||||
--box-shadow: rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px;
|
--black-4: #3a3b3c;
|
||||||
--border-color: #2f2f2f;
|
|
||||||
|
--size-1: 8px;
|
||||||
|
--size-2: 16px;
|
||||||
|
--size-3: 18px;
|
||||||
|
--size-4: 20px;
|
||||||
|
--size-5: 24px;
|
||||||
|
--size-6: 30px;
|
||||||
|
|
||||||
|
--box-shadow-1: rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px;
|
||||||
|
--box-shadow-2: rgba(0, 0, 0, 0.35) 0px 30px 60px -12px inset,
|
||||||
|
rgba(20, 20, 20, 0.3) 0px 18px 36px -18px inset;
|
||||||
|
|
||||||
|
--text-shadow: 3px 2px 8px rgba(50, 50, 50, 0.8);
|
||||||
|
--background: linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96)), url("/banner.png");
|
||||||
|
--profile-size: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@ -25,8 +40,12 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--dark-one);
|
background: var(--black-1);
|
||||||
font-family: "Ubuntu", sans-serif;
|
font-family: "Ubuntu", sans-serif;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@ -35,7 +54,6 @@ body {
|
|||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(100, 100, 100, 0.5);
|
background: rgba(100, 100, 100, 0.5);
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@ -45,261 +63,45 @@ body {
|
|||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: #181818;
|
background: var(--black-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: #282828;
|
background: var(--black-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes colorAnimation {
|
a {
|
||||||
100%,
|
font-weight: 900;
|
||||||
0% {
|
color: var(--white-1);
|
||||||
color: rgb(255, 0, 0);
|
text-decoration: none;
|
||||||
}
|
cursor: pointer;
|
||||||
8% {
|
|
||||||
color: rgb(255, 127, 0);
|
|
||||||
}
|
|
||||||
16% {
|
|
||||||
color: rgb(255, 255, 0);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
color: rgb(127, 255, 0);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
color: rgb(0, 255, 0);
|
|
||||||
}
|
|
||||||
41% {
|
|
||||||
color: rgb(0, 255, 127);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
color: rgb(0, 255, 255);
|
|
||||||
}
|
|
||||||
58% {
|
|
||||||
color: rgb(0, 127, 255);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
color: rgb(0, 0, 255);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
color: rgb(127, 0, 255);
|
|
||||||
}
|
|
||||||
83% {
|
|
||||||
color: rgb(255, 0, 255);
|
|
||||||
}
|
|
||||||
91% {
|
|
||||||
color: rgb(255, 0, 127);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes borderAnimation {
|
a:hover {
|
||||||
100%,
|
text-decoration: underline;
|
||||||
0% {
|
text-shadow: var(--text-shadow);
|
||||||
border-bottom-color: rgb(255, 0, 0);
|
|
||||||
}
|
|
||||||
8% {
|
|
||||||
border-bottom-color: rgb(255, 127, 0);
|
|
||||||
}
|
|
||||||
16% {
|
|
||||||
border-bottom-color: rgb(255, 255, 0);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
border-bottom-color: rgb(127, 255, 0);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
border-bottom-color: rgb(0, 255, 0);
|
|
||||||
}
|
|
||||||
41% {
|
|
||||||
border-bottom-color: rgb(0, 255, 127);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
border-bottom-color: rgb(0, 255, 255);
|
|
||||||
}
|
|
||||||
58% {
|
|
||||||
border-bottom-color: rgb(0, 127, 255);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
border-bottom-color: rgb(0, 0, 255);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
border-bottom-color: rgb(127, 0, 255);
|
|
||||||
}
|
|
||||||
83% {
|
|
||||||
border-bottom-color: rgb(255, 0, 255);
|
|
||||||
}
|
|
||||||
91% {
|
|
||||||
border-bottom-color: rgb(255, 0, 127);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fullBorderAnimation {
|
i .nf {
|
||||||
100%,
|
font-weight: 900;
|
||||||
0% {
|
|
||||||
border-color: rgb(255, 0, 0);
|
|
||||||
}
|
|
||||||
8% {
|
|
||||||
border-color: rgb(255, 127, 0);
|
|
||||||
}
|
|
||||||
16% {
|
|
||||||
border-color: rgb(255, 255, 0);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
border-color: rgb(127, 255, 0);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
border-color: rgb(0, 255, 0);
|
|
||||||
}
|
|
||||||
41% {
|
|
||||||
border-color: rgb(0, 255, 127);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
border-color: rgb(0, 255, 255);
|
|
||||||
}
|
|
||||||
58% {
|
|
||||||
border-color: rgb(0, 127, 255);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
border-color: rgb(0, 0, 255);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
border-color: rgb(127, 0, 255);
|
|
||||||
}
|
|
||||||
83% {
|
|
||||||
border-color: rgb(255, 0, 255);
|
|
||||||
}
|
|
||||||
91% {
|
|
||||||
border-color: rgb(255, 0, 127);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes gayShadowAnimation {
|
ul {
|
||||||
100%,
|
list-style: inside;
|
||||||
0% {
|
margin: 12px 0 12px 0;
|
||||||
box-shadow: rgba(255, 0, 0, 0.07) 0px 1px 2px,
|
|
||||||
rgba(255, 0, 0, 0.07) 0px 2px 4px, rgba(255, 0, 0, 0.07) 0px 4px 8px,
|
|
||||||
rgba(255, 0, 0, 0.07) 0px 8px 16px, rgba(255, 0, 0, 0.07) 0px 16px 32px,
|
|
||||||
rgba(255, 0, 0, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
8% {
|
|
||||||
box-shadow: rgba(255, 127, 0, 0.07) 0px 1px 2px,
|
|
||||||
rgba(255, 127, 0, 0.07) 0px 2px 4px, rgba(255, 127, 0, 0.07) 0px 4px 8px,
|
|
||||||
rgba(255, 127, 0, 0.07) 0px 8px 16px,
|
|
||||||
rgba(255, 127, 0, 0.07) 0px 16px 32px,
|
|
||||||
rgba(255, 127, 0, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
16% {
|
|
||||||
box-shadow: rgba(255, 255, 0, 0.07) 0px 1px 2px,
|
|
||||||
rgba(255, 255, 0, 0.07) 0px 2px 4px, rgba(255, 255, 0, 0.07) 0px 4px 8px,
|
|
||||||
rgba(255, 255, 0, 0.07) 0px 8px 16px,
|
|
||||||
rgba(255, 255, 0, 0.07) 0px 16px 32px,
|
|
||||||
rgba(255, 255, 0, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
box-shadow: rgba(127, 255, 0, 0.07) 0px 1px 2px,
|
|
||||||
rgba(127, 255, 0, 0.07) 0px 2px 4px, rgba(127, 255, 0, 0.07) 0px 4px 8px,
|
|
||||||
rgba(127, 255, 0, 0.07) 0px 8px 16px,
|
|
||||||
rgba(127, 255, 0, 0.07) 0px 16px 32px,
|
|
||||||
rgba(127, 255, 0, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
box-shadow: rgba(0, 255, 0, 0.07) 0px 1px 2px,
|
|
||||||
rgba(0, 255, 0, 0.07) 0px 2px 4px, rgba(0, 255, 0, 0.07) 0px 4px 8px,
|
|
||||||
rgba(0, 255, 0, 0.07) 0px 8px 16px, rgba(0, 255, 0, 0.07) 0px 16px 32px,
|
|
||||||
rgba(0, 255, 0, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
41% {
|
|
||||||
box-shadow: rgba(0, 255, 127, 0.07) 0px 1px 2px,
|
|
||||||
rgba(0, 255, 127, 0.07) 0px 2px 4px, rgba(0, 255, 127, 0.07) 0px 4px 8px,
|
|
||||||
rgba(0, 255, 127, 0.07) 0px 8px 16px,
|
|
||||||
rgba(0, 255, 127, 0.07) 0px 16px 32px,
|
|
||||||
rgba(0, 255, 127, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: rgba(0, 255, 255, 0.07) 0px 1px 2px,
|
|
||||||
rgba(0, 255, 255, 0.07) 0px 2px 4px, rgba(0, 255, 255, 0.07) 0px 4px 8px,
|
|
||||||
rgba(0, 255, 255, 0.07) 0px 8px 16px,
|
|
||||||
rgba(0, 255, 255, 0.07) 0px 16px 32px,
|
|
||||||
rgba(0, 255, 255, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
58% {
|
|
||||||
box-shadow: rgba(0, 127, 255, 0.07) 0px 1px 2px,
|
|
||||||
rgba(0, 127, 255, 0.07) 0px 2px 4px, rgba(0, 127, 255, 0.07) 0px 4px 8px,
|
|
||||||
rgba(0, 127, 255, 0.07) 0px 8px 16px,
|
|
||||||
rgba(0, 127, 255, 0.07) 0px 16px 32px,
|
|
||||||
rgba(0, 127, 255, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
box-shadow: rgba(0, 0, 255, 0.07) 0px 1px 2px,
|
|
||||||
rgba(0, 0, 255, 0.07) 0px 2px 4px, rgba(0, 0, 255, 0.07) 0px 4px 8px,
|
|
||||||
rgba(0, 0, 255, 0.07) 0px 8px 16px, rgba(0, 0, 255, 0.07) 0px 16px 32px,
|
|
||||||
rgba(0, 0, 255, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
box-shadow: rgba(127, 0, 255, 0.07) 0px 1px 2px,
|
|
||||||
rgba(127, 0, 255, 0.07) 0px 2px 4px, rgba(127, 0, 255, 0.07) 0px 4px 8px,
|
|
||||||
rgba(127, 0, 255, 0.07) 0px 8px 16px,
|
|
||||||
rgba(127, 0, 255, 0.07) 0px 16px 32px,
|
|
||||||
rgba(127, 0, 255, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
83% {
|
|
||||||
box-shadow: rgba(255, 0, 255, 0.07) 0px 1px 2px,
|
|
||||||
rgba(255, 0, 255, 0.07) 0px 2px 4px, rgba(255, 0, 255, 0.07) 0px 4px 8px,
|
|
||||||
rgba(255, 0, 255, 0.07) 0px 8px 16px,
|
|
||||||
rgba(255, 0, 255, 0.07) 0px 16px 32px,
|
|
||||||
rgba(255, 0, 255, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
91% {
|
|
||||||
box-shadow: rgba(255, 0, 127, 0.07) 0px 1px 2px,
|
|
||||||
rgba(255, 0, 127, 0.07) 0px 2px 4px, rgba(255, 0, 127, 0.07) 0px 4px 8px,
|
|
||||||
rgba(255, 0, 127, 0.07) 0px 8px 16px,
|
|
||||||
rgba(255, 0, 127, 0.07) 0px 16px 32px,
|
|
||||||
rgba(255, 0, 127, 0.07) 0px 32px 64px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes underlineAnimation {
|
li + li {
|
||||||
100%,
|
margin-top: 10px;
|
||||||
0% {
|
|
||||||
text-decoration-color: rgb(255, 0, 0);
|
|
||||||
}
|
|
||||||
8% {
|
|
||||||
text-decoration-color: rgb(255, 127, 0);
|
|
||||||
}
|
|
||||||
16% {
|
|
||||||
text-decoration-color: rgb(255, 255, 0);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
text-decoration-color: rgb(127, 255, 0);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
text-decoration-color: rgb(0, 255, 0);
|
|
||||||
}
|
|
||||||
41% {
|
|
||||||
text-decoration-color: rgb(0, 255, 127);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
text-decoration-color: rgb(0, 255, 255);
|
|
||||||
}
|
|
||||||
58% {
|
|
||||||
text-decoration-color: rgb(0, 127, 255);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
text-decoration-color: rgb(0, 0, 255);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
text-decoration-color: rgb(127, 0, 255);
|
|
||||||
}
|
|
||||||
83% {
|
|
||||||
text-decoration-color: rgb(255, 0, 255);
|
|
||||||
}
|
|
||||||
91% {
|
|
||||||
text-decoration-color: rgb(255, 0, 127);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.c, c {
|
input {
|
||||||
animation-name: colorAnimation;
|
background: var(--black-3);
|
||||||
animation-iteration-count: infinite;
|
border: none;
|
||||||
animation-duration: 10s;
|
outline: none;
|
||||||
|
font-size: var(--size-4);
|
||||||
|
padding: 10px;
|
||||||
|
border: solid 1px var(--black-4);
|
||||||
|
color: var(--white-1);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
color: #c9d1d9;
|
color: #c9d1d9;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial,
|
||||||
|
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
@ -38,7 +39,7 @@
|
|||||||
.markdown-body h6:hover .anchor .octicon-link:before {
|
.markdown-body h6:hover .anchor .octicon-link:before {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
content: ' ';
|
content: " ";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: currentColor;
|
background-color: currentColor;
|
||||||
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
|
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
|
||||||
@ -60,11 +61,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body a {
|
.markdown-body a {
|
||||||
animation-name: colorAnimation;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
animation-duration: 10s;
|
|
||||||
background-color: transparent;
|
|
||||||
color: #58a6ff;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,15 +79,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h1 {
|
.markdown-body h1 {
|
||||||
margin: .67em 0;
|
margin: 0.67em 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-bottom: .3em;
|
padding-bottom: 0.3em;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
border-bottom: 1px solid #21262d;
|
border-bottom: 1px solid #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body mark {
|
.markdown-body mark {
|
||||||
background-color: rgba(187,128,9,0.15);
|
background-color: rgba(187, 128, 9, 0.15);
|
||||||
color: #c9d1d9;
|
color: #c9d1d9;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +135,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-bottom: 1px solid #21262d;
|
border-bottom: 1px solid #21262d;
|
||||||
height: .25em;
|
height: 0.25em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 24px 0;
|
margin: 24px 0;
|
||||||
background-color: #30363d;
|
background-color: #30363d;
|
||||||
@ -155,31 +151,31 @@
|
|||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body [type=button],
|
.markdown-body [type="button"],
|
||||||
.markdown-body [type=reset],
|
.markdown-body [type="reset"],
|
||||||
.markdown-body [type=submit] {
|
.markdown-body [type="submit"] {
|
||||||
-webkit-appearance: button;
|
-webkit-appearance: button;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body [type=checkbox],
|
.markdown-body [type="checkbox"],
|
||||||
.markdown-body [type=radio] {
|
.markdown-body [type="radio"] {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body [type=number]::-webkit-inner-spin-button,
|
.markdown-body [type="number"]::-webkit-inner-spin-button,
|
||||||
.markdown-body [type=number]::-webkit-outer-spin-button {
|
.markdown-body [type="number"]::-webkit-outer-spin-button {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body [type=search]::-webkit-search-cancel-button,
|
.markdown-body [type="search"]::-webkit-search-cancel-button,
|
||||||
.markdown-body [type=search]::-webkit-search-decoration {
|
.markdown-body [type="search"]::-webkit-search-decoration {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ::-webkit-input-placeholder {
|
.markdown-body ::-webkit-input-placeholder {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
opacity: .54;
|
opacity: 0.54;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ::-webkit-file-upload-button {
|
.markdown-body ::-webkit-file-upload-button {
|
||||||
@ -225,30 +221,30 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body details:not([open])>*:not(summary) {
|
.markdown-body details:not([open]) > *:not(summary) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body a:focus,
|
.markdown-body a:focus,
|
||||||
.markdown-body [role=button]:focus,
|
.markdown-body [role="button"]:focus,
|
||||||
.markdown-body input[type=radio]:focus,
|
.markdown-body input[type="radio"]:focus,
|
||||||
.markdown-body input[type=checkbox]:focus {
|
.markdown-body input[type="checkbox"]:focus {
|
||||||
outline: 2px solid #58a6ff;
|
outline: 2px solid #58a6ff;
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body a:focus:not(:focus-visible),
|
.markdown-body a:focus:not(:focus-visible),
|
||||||
.markdown-body [role=button]:focus:not(:focus-visible),
|
.markdown-body [role="button"]:focus:not(:focus-visible),
|
||||||
.markdown-body input[type=radio]:focus:not(:focus-visible),
|
.markdown-body input[type="radio"]:focus:not(:focus-visible),
|
||||||
.markdown-body input[type=checkbox]:focus:not(:focus-visible) {
|
.markdown-body input[type="checkbox"]:focus:not(:focus-visible) {
|
||||||
outline: solid 1px transparent;
|
outline: solid 1px transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body a:focus-visible,
|
.markdown-body a:focus-visible,
|
||||||
.markdown-body [role=button]:focus-visible,
|
.markdown-body [role="button"]:focus-visible,
|
||||||
.markdown-body input[type=radio]:focus-visible,
|
.markdown-body input[type="radio"]:focus-visible,
|
||||||
.markdown-body input[type=checkbox]:focus-visible {
|
.markdown-body input[type="checkbox"]:focus-visible {
|
||||||
outline: 2px solid #58a6ff;
|
outline: 2px solid #58a6ff;
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@ -256,25 +252,32 @@
|
|||||||
|
|
||||||
.markdown-body a:not([class]):focus,
|
.markdown-body a:not([class]):focus,
|
||||||
.markdown-body a:not([class]):focus-visible,
|
.markdown-body a:not([class]):focus-visible,
|
||||||
.markdown-body input[type=radio]:focus,
|
.markdown-body input[type="radio"]:focus,
|
||||||
.markdown-body input[type=radio]:focus-visible,
|
.markdown-body input[type="radio"]:focus-visible,
|
||||||
.markdown-body input[type=checkbox]:focus,
|
.markdown-body input[type="checkbox"]:focus,
|
||||||
.markdown-body input[type=checkbox]:focus-visible {
|
.markdown-body input[type="checkbox"]:focus-visible {
|
||||||
outline-offset: 0;
|
outline-offset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body kbd {
|
.markdown-body kbd {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
font:
|
||||||
|
11px ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
SF Mono,
|
||||||
|
Menlo,
|
||||||
|
Consolas,
|
||||||
|
Liberation Mono,
|
||||||
|
monospace;
|
||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
color: #c9d1d9;
|
color: #c9d1d9;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
background-color: #161b22;
|
background-color: #161b22;
|
||||||
border: solid 1px rgba(110,118,129,0.4);
|
border: solid 1px rgba(110, 118, 129, 0.4);
|
||||||
border-bottom-color: rgba(110,118,129,0.4);
|
border-bottom-color: rgba(110, 118, 129, 0.4);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: inset 0 -1px 0 rgba(110,118,129,0.4);
|
box-shadow: inset 0 -1px 0 rgba(110, 118, 129, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h1,
|
.markdown-body h1,
|
||||||
@ -291,7 +294,7 @@
|
|||||||
|
|
||||||
.markdown-body h2 {
|
.markdown-body h2 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-bottom: .3em;
|
padding-bottom: 0.3em;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
border-bottom: 1px solid #21262d;
|
border-bottom: 1px solid #21262d;
|
||||||
}
|
}
|
||||||
@ -308,12 +311,12 @@
|
|||||||
|
|
||||||
.markdown-body h5 {
|
.markdown-body h5 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: .875em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h6 {
|
.markdown-body h6 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: .85em;
|
font-size: 0.85em;
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,7 +329,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
border-left: .25em solid #30363d;
|
border-left: 0.25em solid #30363d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ul,
|
.markdown-body ul,
|
||||||
@ -355,14 +358,28 @@
|
|||||||
.markdown-body tt,
|
.markdown-body tt,
|
||||||
.markdown-body code,
|
.markdown-body code,
|
||||||
.markdown-body samp {
|
.markdown-body samp {
|
||||||
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
font-family:
|
||||||
|
ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
SF Mono,
|
||||||
|
Menlo,
|
||||||
|
Consolas,
|
||||||
|
Liberation Mono,
|
||||||
|
monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body pre {
|
.markdown-body pre {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
font-family:
|
||||||
|
ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
SF Mono,
|
||||||
|
Menlo,
|
||||||
|
Consolas,
|
||||||
|
Liberation Mono,
|
||||||
|
monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
}
|
}
|
||||||
@ -392,11 +409,11 @@
|
|||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body>*:first-child {
|
.markdown-body > *:first-child {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body>*:last-child {
|
.markdown-body > *:last-child {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,11 +449,11 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body blockquote>:first-child {
|
.markdown-body blockquote > :first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body blockquote>:last-child {
|
.markdown-body blockquote > :last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,7 +498,7 @@
|
|||||||
.markdown-body h5 code,
|
.markdown-body h5 code,
|
||||||
.markdown-body h6 tt,
|
.markdown-body h6 tt,
|
||||||
.markdown-body h6 code {
|
.markdown-body h6 code {
|
||||||
padding: 0 .2em;
|
padding: 0 0.2em;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -515,19 +532,19 @@
|
|||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ol[type=a] {
|
.markdown-body ol[type="a"] {
|
||||||
list-style-type: lower-alpha;
|
list-style-type: lower-alpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ol[type=A] {
|
.markdown-body ol[type="A"] {
|
||||||
list-style-type: upper-alpha;
|
list-style-type: upper-alpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ol[type=i] {
|
.markdown-body ol[type="i"] {
|
||||||
list-style-type: lower-roman;
|
list-style-type: lower-roman;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body ol[type=I] {
|
.markdown-body ol[type="I"] {
|
||||||
list-style-type: upper-roman;
|
list-style-type: upper-roman;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -535,7 +552,7 @@
|
|||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body div>ol:not([type]) {
|
.markdown-body div > ol:not([type]) {
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -547,12 +564,12 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body li>p {
|
.markdown-body li > p {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body li+li {
|
.markdown-body li + li {
|
||||||
margin-top: .25em;
|
margin-top: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body dl {
|
.markdown-body dl {
|
||||||
@ -595,11 +612,11 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body img[align=right] {
|
.markdown-body img[align="right"] {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body img[align=left] {
|
.markdown-body img[align="left"] {
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -614,7 +631,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body span.frame>span {
|
.markdown-body span.frame > span {
|
||||||
display: block;
|
display: block;
|
||||||
float: left;
|
float: left;
|
||||||
width: auto;
|
width: auto;
|
||||||
@ -642,7 +659,7 @@
|
|||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body span.align-center>span {
|
.markdown-body span.align-center > span {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 13px auto 0;
|
margin: 13px auto 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -660,7 +677,7 @@
|
|||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body span.align-right>span {
|
.markdown-body span.align-right > span {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 13px 0 0;
|
margin: 13px 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -690,7 +707,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body span.float-right>span {
|
.markdown-body span.float-right > span {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 13px auto 0;
|
margin: 13px auto 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -699,11 +716,11 @@
|
|||||||
|
|
||||||
.markdown-body code,
|
.markdown-body code,
|
||||||
.markdown-body tt {
|
.markdown-body tt {
|
||||||
padding: .2em .4em;
|
padding: 0.2em 0.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
background: var(--dark-two);
|
background: var(--black-3);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -724,7 +741,7 @@
|
|||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body pre>code {
|
.markdown-body pre > code {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
@ -748,8 +765,7 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
background-color: var(--dark-two);
|
background-color: var(--black-3);
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body pre code,
|
.markdown-body pre code,
|
||||||
@ -963,7 +979,7 @@
|
|||||||
.markdown-body g-emoji {
|
.markdown-body g-emoji {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 1ch;
|
min-width: 1ch;
|
||||||
font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
|
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-style: normal !important;
|
font-style: normal !important;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -988,7 +1004,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .task-list-item+.task-list-item {
|
.markdown-body .task-list-item + .task-list-item {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -997,12 +1013,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .task-list-item-checkbox {
|
.markdown-body .task-list-item-checkbox {
|
||||||
margin: 0 .2em .25em -1.4em;
|
margin: 0 0.2em 0.25em -1.4em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
|
.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
|
||||||
margin: 0 -1.6em .25em .2em;
|
margin: 0 -1.6em 0.25em 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .contains-task-list {
|
.markdown-body .contains-task-list {
|
||||||
|
BIN
app/static/profile/cool.png
Normal file
BIN
app/static/profile/cool.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
app/static/profile/money.png
Normal file
BIN
app/static/profile/money.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
app/static/profile/reader.png
Normal file
BIN
app/static/profile/reader.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
app/static/profile/sad.png
Normal file
BIN
app/static/profile/sad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
app/static/profile/tired.png
Normal file
BIN
app/static/profile/tired.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@ -1,14 +1,14 @@
|
|||||||
//import adapter from '@sveltejs/adapter-auto';
|
//import adapter from '@sveltejs/adapter-auto';
|
||||||
import adapter from '@sveltejs/adapter-node';
|
import adapter from "@sveltejs/adapter-node";
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter()
|
adapter: adapter(),
|
||||||
},
|
},
|
||||||
onwarn: (warning, handler) => {
|
onwarn: (warning, handler) => {
|
||||||
if (warning.code === "a11y-click-events-have-key-events") return
|
if (warning.code === "a11y-click-events-have-key-events") return;
|
||||||
handler(warning)
|
handler(warning);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,36 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from "vite";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
|
||||||
|
const default_env = {
|
||||||
|
REPORT_URL: "https://github.com/ngn13/website/issues",
|
||||||
|
SOURCE_URL: "https://github.com/ngn13/website",
|
||||||
|
APP_URL: "http://localhost:7001",
|
||||||
|
API_URL: "http://localhost:7002",
|
||||||
|
DOC_URL: "http://localhost:7003",
|
||||||
|
};
|
||||||
|
|
||||||
|
const file = fileURLToPath(new URL("package.json", import.meta.url));
|
||||||
|
const json = readFileSync(file, "utf8");
|
||||||
|
const pkg = JSON.parse(json);
|
||||||
|
|
||||||
|
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()],
|
||||||
|
envPrefix: "WEBSITE",
|
||||||
|
preview: {
|
||||||
|
port: 7001,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 7001,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
pkg: pkg,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
61
deploy/compose.yml
Normal file
61
deploy/compose.yml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: "website_app"
|
||||||
|
image: website_app
|
||||||
|
build:
|
||||||
|
context: ./app
|
||||||
|
args:
|
||||||
|
WEBSITE_SOURCE_URL: "http://github.com/ngn13/website"
|
||||||
|
WEBSITE_REPORT_URL: "http://github.com/ngn13/website/issues"
|
||||||
|
WEBSITE_APP_URL: "http://localhost:7001"
|
||||||
|
WEBSITE_API_URL: "http://localhost:7002"
|
||||||
|
WEBSITE_DOC_URL: "http://doc:7003"
|
||||||
|
security_opt:
|
||||||
|
- "no-new-privileges:true"
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:7001:7001"
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
- doc
|
||||||
|
|
||||||
|
api:
|
||||||
|
container_name: "website_api"
|
||||||
|
image: website_api
|
||||||
|
build:
|
||||||
|
context: ./api
|
||||||
|
security_opt:
|
||||||
|
- "no-new-privileges:true"
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:7002:7002"
|
||||||
|
volumes:
|
||||||
|
- ./data.db:/api/data.db:rw
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
WEBSITE_DEBUG: "false"
|
||||||
|
WEBSITE_APP_URL: "http://localhost:7001/"
|
||||||
|
WEBSITE_PASSWORD: "change_me"
|
||||||
|
WEBSITE_HOST: "0.0.0.0:7002"
|
||||||
|
WEBSITE_IP_HEADER: "X-Real-IP"
|
||||||
|
WEBSITE_INTERVAL: "1h"
|
||||||
|
WEBSITE_TIMEOUT: "15s"
|
||||||
|
WEBSITE_LIMIT: "5s"
|
||||||
|
|
||||||
|
doc:
|
||||||
|
container_name: "website_doc"
|
||||||
|
image: website_doc
|
||||||
|
read_only: true
|
||||||
|
build:
|
||||||
|
context: ./doc
|
||||||
|
security_opt:
|
||||||
|
- "no-new-privileges:true"
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
WEBSITE_HOST: "0.0.0.0:7003"
|
||||||
|
WEBSITE_DOCS_DIR: "./docs"
|
9
deploy/run.sh
Normal file
9
deploy/run.sh
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! -f data.db ]; then
|
||||||
|
touch data.db
|
||||||
|
sudo chmod 1001:1001 data.db
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
225
doc/.clang-format
Normal file
225
doc/.clang-format
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
---
|
||||||
|
Language: Cpp
|
||||||
|
# BasedOnStyle: LLVM
|
||||||
|
AccessModifierOffset: -2
|
||||||
|
AlignAfterOpenBracket: DontAlign
|
||||||
|
AlignArrayOfStructures: Left
|
||||||
|
AlignConsecutiveAssignments:
|
||||||
|
Enabled: true
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
PadOperators: true
|
||||||
|
AlignConsecutiveBitFields:
|
||||||
|
Enabled: true
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveDeclarations:
|
||||||
|
Enabled: true
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveMacros:
|
||||||
|
Enabled: true
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignEscapedNewlines: Right
|
||||||
|
AlignOperands: Align
|
||||||
|
AlignTrailingComments:
|
||||||
|
Kind: Always
|
||||||
|
OverEmptyLines: 0
|
||||||
|
AllowAllArgumentsOnNextLine: true
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: true
|
||||||
|
AllowShortBlocksOnASingleLine: Never
|
||||||
|
AllowShortCaseLabelsOnASingleLine: false
|
||||||
|
AllowShortEnumsOnASingleLine: true
|
||||||
|
AllowShortFunctionsOnASingleLine: None
|
||||||
|
AllowShortIfStatementsOnASingleLine: Never
|
||||||
|
AllowShortLambdasOnASingleLine: All
|
||||||
|
AllowShortLoopsOnASingleLine: false
|
||||||
|
AlwaysBreakAfterDefinitionReturnType: None
|
||||||
|
AlwaysBreakAfterReturnType: None
|
||||||
|
AlwaysBreakBeforeMultilineStrings: false
|
||||||
|
AlwaysBreakTemplateDeclarations: MultiLine
|
||||||
|
AttributeMacros:
|
||||||
|
- __capability
|
||||||
|
BinPackArguments: false
|
||||||
|
BinPackParameters: true
|
||||||
|
BitFieldColonSpacing: Both
|
||||||
|
BraceWrapping:
|
||||||
|
AfterCaseLabel: false
|
||||||
|
AfterClass: false
|
||||||
|
AfterControlStatement: Never
|
||||||
|
AfterEnum: false
|
||||||
|
AfterExternBlock: false
|
||||||
|
AfterFunction: false
|
||||||
|
AfterNamespace: false
|
||||||
|
AfterObjCDeclaration: false
|
||||||
|
AfterStruct: false
|
||||||
|
AfterUnion: false
|
||||||
|
BeforeCatch: false
|
||||||
|
BeforeElse: false
|
||||||
|
BeforeLambdaBody: false
|
||||||
|
BeforeWhile: false
|
||||||
|
IndentBraces: false
|
||||||
|
SplitEmptyFunction: true
|
||||||
|
SplitEmptyRecord: true
|
||||||
|
SplitEmptyNamespace: true
|
||||||
|
BreakAfterAttributes: Never
|
||||||
|
BreakAfterJavaFieldAnnotations: false
|
||||||
|
BreakArrays: true
|
||||||
|
BreakBeforeBinaryOperators: None
|
||||||
|
BreakBeforeConceptDeclarations: Always
|
||||||
|
BreakBeforeBraces: Attach
|
||||||
|
BreakBeforeInlineASMColon: OnlyMultiline
|
||||||
|
BreakBeforeTernaryOperators: true
|
||||||
|
BreakConstructorInitializers: BeforeColon
|
||||||
|
BreakInheritanceList: BeforeColon
|
||||||
|
BreakStringLiterals: true
|
||||||
|
ColumnLimit: 120
|
||||||
|
CommentPragmas: '^ IWYU pragma:'
|
||||||
|
CompactNamespaces: false
|
||||||
|
ConstructorInitializerIndentWidth: 4
|
||||||
|
ContinuationIndentWidth: 4
|
||||||
|
Cpp11BracedListStyle: true
|
||||||
|
DerivePointerAlignment: false
|
||||||
|
DisableFormat: false
|
||||||
|
EmptyLineAfterAccessModifier: Never
|
||||||
|
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||||
|
ExperimentalAutoDetectBinPacking: false
|
||||||
|
FixNamespaceComments: true
|
||||||
|
ForEachMacros:
|
||||||
|
- foreach
|
||||||
|
- Q_FOREACH
|
||||||
|
- BOOST_FOREACH
|
||||||
|
IfMacros:
|
||||||
|
- KJ_IF_MAYBE
|
||||||
|
IncludeBlocks: Preserve
|
||||||
|
IncludeCategories:
|
||||||
|
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||||
|
Priority: 3
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
- Regex: '.*'
|
||||||
|
Priority: 1
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
IncludeIsMainRegex: '(Test)?$'
|
||||||
|
IncludeIsMainSourceRegex: ''
|
||||||
|
IndentAccessModifiers: false
|
||||||
|
IndentCaseBlocks: false
|
||||||
|
IndentCaseLabels: false
|
||||||
|
IndentExternBlock: AfterExternBlock
|
||||||
|
IndentGotoLabels: true
|
||||||
|
IndentPPDirectives: None
|
||||||
|
IndentRequiresClause: true
|
||||||
|
IndentWidth: 2
|
||||||
|
IndentWrappedFunctionNames: false
|
||||||
|
InsertBraces: false
|
||||||
|
InsertNewlineAtEOF: false
|
||||||
|
InsertTrailingCommas: None
|
||||||
|
IntegerLiteralSeparator:
|
||||||
|
Binary: 0
|
||||||
|
BinaryMinDigits: 0
|
||||||
|
Decimal: 0
|
||||||
|
DecimalMinDigits: 0
|
||||||
|
Hex: 0
|
||||||
|
HexMinDigits: 0
|
||||||
|
JavaScriptQuotes: Leave
|
||||||
|
JavaScriptWrapImports: true
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||||
|
LambdaBodyIndentation: Signature
|
||||||
|
LineEnding: DeriveLF
|
||||||
|
MacroBlockBegin: ''
|
||||||
|
MacroBlockEnd: ''
|
||||||
|
MaxEmptyLinesToKeep: 1
|
||||||
|
NamespaceIndentation: None
|
||||||
|
ObjCBinPackProtocolList: Auto
|
||||||
|
ObjCBlockIndentWidth: 2
|
||||||
|
ObjCBreakBeforeNestedBlockParam: true
|
||||||
|
ObjCSpaceAfterProperty: false
|
||||||
|
ObjCSpaceBeforeProtocolList: true
|
||||||
|
PackConstructorInitializers: BinPack
|
||||||
|
PenaltyBreakAssignment: 2
|
||||||
|
PenaltyBreakBeforeFirstCallParameter: 19
|
||||||
|
PenaltyBreakComment: 300
|
||||||
|
PenaltyBreakFirstLessLess: 120
|
||||||
|
PenaltyBreakOpenParenthesis: 0
|
||||||
|
PenaltyBreakString: 1000
|
||||||
|
PenaltyBreakTemplateDeclaration: 10
|
||||||
|
PenaltyExcessCharacter: 1000000
|
||||||
|
PenaltyIndentedWhitespace: 0
|
||||||
|
PenaltyReturnTypeOnItsOwnLine: 60
|
||||||
|
PointerAlignment: Right
|
||||||
|
PPIndentWidth: -1
|
||||||
|
QualifierAlignment: Leave
|
||||||
|
ReferenceAlignment: Pointer
|
||||||
|
ReflowComments: true
|
||||||
|
RemoveBracesLLVM: false
|
||||||
|
RemoveSemicolon: false
|
||||||
|
RequiresClausePosition: OwnLine
|
||||||
|
RequiresExpressionIndentation: OuterScope
|
||||||
|
SeparateDefinitionBlocks: Leave
|
||||||
|
ShortNamespaceLines: 1
|
||||||
|
SortIncludes: false
|
||||||
|
SortJavaStaticImport: Before
|
||||||
|
SortUsingDeclarations: LexicographicNumeric
|
||||||
|
SpaceAfterCStyleCast: false
|
||||||
|
SpaceAfterLogicalNot: false
|
||||||
|
SpaceAfterTemplateKeyword: true
|
||||||
|
SpaceAroundPointerQualifiers: Default
|
||||||
|
SpaceBeforeAssignmentOperators: true
|
||||||
|
SpaceBeforeCaseColon: false
|
||||||
|
SpaceBeforeCpp11BracedList: false
|
||||||
|
SpaceBeforeCtorInitializerColon: true
|
||||||
|
SpaceBeforeInheritanceColon: true
|
||||||
|
SpaceBeforeParens: ControlStatements
|
||||||
|
SpaceBeforeParensOptions:
|
||||||
|
AfterControlStatements: true
|
||||||
|
AfterForeachMacros: true
|
||||||
|
AfterFunctionDefinitionName: false
|
||||||
|
AfterFunctionDeclarationName: false
|
||||||
|
AfterIfMacros: true
|
||||||
|
AfterOverloadedOperator: false
|
||||||
|
AfterRequiresInClause: false
|
||||||
|
AfterRequiresInExpression: false
|
||||||
|
BeforeNonEmptyParentheses: false
|
||||||
|
SpaceBeforeRangeBasedForLoopColon: true
|
||||||
|
SpaceBeforeSquareBrackets: false
|
||||||
|
SpaceInEmptyBlock: false
|
||||||
|
SpaceInEmptyParentheses: false
|
||||||
|
SpacesBeforeTrailingComments: 1
|
||||||
|
SpacesInAngles: Never
|
||||||
|
SpacesInConditionalStatement: false
|
||||||
|
SpacesInContainerLiterals: true
|
||||||
|
SpacesInCStyleCastParentheses: false
|
||||||
|
SpacesInLineCommentPrefix:
|
||||||
|
Minimum: 1
|
||||||
|
Maximum: -1
|
||||||
|
SpacesInParentheses: false
|
||||||
|
SpacesInSquareBrackets: false
|
||||||
|
Standard: Latest
|
||||||
|
StatementAttributeLikeMacros:
|
||||||
|
- Q_EMIT
|
||||||
|
StatementMacros:
|
||||||
|
- Q_UNUSED
|
||||||
|
- QT_REQUIRE_VERSION
|
||||||
|
TabWidth: 8
|
||||||
|
UseTab: Never
|
||||||
|
WhitespaceSensitiveMacros:
|
||||||
|
- BOOST_PP_STRINGIZE
|
||||||
|
- CF_SWIFT_NAME
|
||||||
|
- NS_SWIFT_NAME
|
||||||
|
- PP_STRINGIZE
|
||||||
|
- STRINGIZE
|
||||||
|
...
|
||||||
|
|
4
doc/.gitignore
vendored
Normal file
4
doc/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
compile_commands.json
|
||||||
|
.cache
|
||||||
|
*.elf
|
||||||
|
dist
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user