Compare commits
10 Commits
fc11748e57
...
95986d170d
Author | SHA1 | Date | |
---|---|---|---|
95986d170d | |||
fb58722216 | |||
fa2f3acb35 | |||
ecaa6fb68f | |||
3398324664 | |||
6f7263dd84 | |||
e87764a4c2 | |||
5fb3c03e40 | |||
ac307de76c | |||
dee3ef4d85 |
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)
|
||||||
![](assets/4.0_index.png)
|
![](assets/4.0_index.png)
|
||||||
|
345
admin/admin.py
345
admin/admin.py
@ -30,8 +30,6 @@ import requests as req
|
|||||||
from os import getenv
|
from os import getenv
|
||||||
from sys import argv
|
from sys import argv
|
||||||
|
|
||||||
API_URL_ENV = "API_URL"
|
|
||||||
|
|
||||||
|
|
||||||
# logger used by the script
|
# logger used by the script
|
||||||
class Log:
|
class Log:
|
||||||
@ -138,11 +136,32 @@ class AdminAPI:
|
|||||||
|
|
||||||
self.PUT("/v1/admin/service/add", service)
|
self.PUT("/v1/admin/service/add", service)
|
||||||
|
|
||||||
def del_service(self, service: str) -> None:
|
def del_service(self, name: str) -> None:
|
||||||
if service == "":
|
if name == "":
|
||||||
raise Exception("Service name cannot be empty")
|
raise Exception("Service name cannot be empty")
|
||||||
|
|
||||||
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(service))
|
self.DELETE("/v1/admin/service/del?name=%s" % quote_plus(name))
|
||||||
|
|
||||||
|
def add_project(self, project: Dict[str, str]):
|
||||||
|
if "name" not in project or project["name"] == "":
|
||||||
|
raise Exception('Project structure is missing required "name" field')
|
||||||
|
|
||||||
|
if "desc" not in project:
|
||||||
|
raise Exception('Project structure is missing required "desc" field')
|
||||||
|
|
||||||
|
if not self._check_multilang_field(project["desc"]):
|
||||||
|
raise Exception(
|
||||||
|
'Project structure field "desc" needs at least '
|
||||||
|
+ "one supported language entry"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.PUT("/v1/admin/project/add", project)
|
||||||
|
|
||||||
|
def del_project(self, name: str) -> None:
|
||||||
|
if name == "":
|
||||||
|
raise Exception("Project name cannot be empty")
|
||||||
|
|
||||||
|
self.DELETE("/v1/admin/project/del?name=%s" % quote_plus(name))
|
||||||
|
|
||||||
def check_services(self) -> None:
|
def check_services(self) -> None:
|
||||||
self.GET("/v1/admin/service/check")
|
self.GET("/v1/admin/service/check")
|
||||||
@ -174,184 +193,212 @@ class AdminAPI:
|
|||||||
|
|
||||||
self.PUT("/v1/admin/news/add", news)
|
self.PUT("/v1/admin/news/add", news)
|
||||||
|
|
||||||
def del_news(self, news: str) -> None:
|
def del_news(self, id: str) -> None:
|
||||||
if news == "":
|
if id == "":
|
||||||
raise Exception("News ID cannot be empty")
|
raise Exception("News ID cannot be empty")
|
||||||
|
|
||||||
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(news))
|
self.DELETE("/v1/admin/news/del?id=%s" % quote_plus(id))
|
||||||
|
|
||||||
def logs(self) -> List[Dict[str, Any]]:
|
def logs(self) -> List[Dict[str, Any]]:
|
||||||
return self.GET("/v1/admin/logs")
|
return self.GET("/v1/admin/logs")
|
||||||
|
|
||||||
|
|
||||||
# local helper functions used by the script
|
class AdminScript:
|
||||||
def __format_time(ts: int) -> str:
|
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")
|
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
|
||||||
|
|
||||||
|
def __load_json_file(self, file: str) -> Dict[str, Any]:
|
||||||
def __load_json_file(file: str) -> Dict[str, Any]:
|
|
||||||
with open(file, "r") as f:
|
with open(file, "r") as f:
|
||||||
data = loads(f.read())
|
data = loads(f.read())
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def __dump_json_file(self, data: Dict[str, Any], file: str) -> None:
|
||||||
def __dump_json_file(data: Dict[str, Any], file: str) -> None:
|
|
||||||
with open(file, "w") as f:
|
with open(file, "w") as f:
|
||||||
data = dumps(data, indent=2)
|
data = dumps(data, indent=2)
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
||||||
|
def run(self) -> bool:
|
||||||
# command handlers
|
|
||||||
def __handle_command(log: Log, api: AdminAPI, cmd: str) -> None:
|
|
||||||
match cmd:
|
|
||||||
case "add_service":
|
|
||||||
data: Dict[str, str] = {}
|
|
||||||
data["desc"] = {}
|
|
||||||
|
|
||||||
data["name"] = log.input("Serivce name")
|
|
||||||
for lang in api.languages:
|
|
||||||
data["desc"][lang] = log.input("Serivce desc (%s)" % lang)
|
|
||||||
data["check_url"] = log.input("Serivce status check URL")
|
|
||||||
data["clear"] = log.input("Serivce clearnet URL")
|
|
||||||
data["onion"] = log.input("Serivce onion URL")
|
|
||||||
data["i2p"] = log.input("Serivce I2P URL")
|
|
||||||
|
|
||||||
api.add_service(data)
|
|
||||||
log.info("Service has been added")
|
|
||||||
|
|
||||||
case "del_service":
|
|
||||||
api.del_service(log.input("Serivce name"))
|
|
||||||
log.info("Service has been deleted")
|
|
||||||
|
|
||||||
case "check_services":
|
|
||||||
api.check_services()
|
|
||||||
log.info("Requested status check for all the services")
|
|
||||||
|
|
||||||
case "add_news":
|
|
||||||
news: Dict[str, str] = {}
|
|
||||||
news["title"] = {}
|
|
||||||
news["content"] = {}
|
|
||||||
|
|
||||||
data["id"] = log.input("News ID")
|
|
||||||
for lang in api.languages:
|
|
||||||
data["title"][lang] = log.input("News title (%s)" % lang)
|
|
||||||
data["author"] = log.input("News author")
|
|
||||||
for lang in api.languages:
|
|
||||||
data["content"][lang] = log.input("News content (%s)" % lang)
|
|
||||||
|
|
||||||
api.add_news(data)
|
|
||||||
log.info("News has been added")
|
|
||||||
|
|
||||||
case "del_news":
|
|
||||||
api.del_news(log.input("News ID"))
|
|
||||||
log.info("News has been deleted")
|
|
||||||
|
|
||||||
case "logs":
|
|
||||||
logs = api.logs()
|
|
||||||
|
|
||||||
if logs["result"] is None or len(logs["result"]) == 0:
|
|
||||||
return log.info("No available logs")
|
|
||||||
|
|
||||||
for log in logs["result"]:
|
|
||||||
log.info(
|
|
||||||
"Time: %s | Action: %s"
|
|
||||||
% (__format_time(log["time"]), log["action"])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def __handle_command_with_file(log: Log, api: AdminAPI, cmd: str, file: str) -> None:
|
|
||||||
match cmd:
|
|
||||||
case "add_service":
|
|
||||||
data = __load_json_file(file)
|
|
||||||
api.add_service(data)
|
|
||||||
log.info("Service has been added")
|
|
||||||
|
|
||||||
case "del_service":
|
|
||||||
data = __load_json_file(file)
|
|
||||||
api.del_service(data["name"])
|
|
||||||
log.info("Service has been deleted")
|
|
||||||
|
|
||||||
case "check_services":
|
|
||||||
api.check_services()
|
|
||||||
log.info("Requested status check for all the services")
|
|
||||||
|
|
||||||
case "add_news":
|
|
||||||
data = __load_json_file(file)
|
|
||||||
api.add_news(data)
|
|
||||||
log.info("News has been added")
|
|
||||||
|
|
||||||
case "del_news":
|
|
||||||
data = __load_json_file(file)
|
|
||||||
api.del_news(data["id"])
|
|
||||||
log.info("News has been deleted")
|
|
||||||
|
|
||||||
case "logs":
|
|
||||||
logs = api.logs()
|
|
||||||
|
|
||||||
if logs["result"] is None or len(logs["result"]) == 0:
|
|
||||||
return log.info("No available logs")
|
|
||||||
|
|
||||||
__dump_json_file(logs["result"], file)
|
|
||||||
log.info("Logs has been saved")
|
|
||||||
|
|
||||||
|
|
||||||
commands = [
|
|
||||||
"add_service",
|
|
||||||
"del_service",
|
|
||||||
"check_services",
|
|
||||||
"add_news",
|
|
||||||
"del_news",
|
|
||||||
"logs"
|
|
||||||
]
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
log = Log()
|
|
||||||
|
|
||||||
if len(argv) < 2 or len(argv) > 3:
|
if len(argv) < 2 or len(argv) > 3:
|
||||||
log.error("Usage: %s [command] <file>" % argv[0])
|
self.log.error("Usage: %s [command] <file>" % argv[0])
|
||||||
log.info("Here is a list of available commands:")
|
self.log.info("Here is a list of available commands:")
|
||||||
print("\tadd_service")
|
|
||||||
print("\tdel_service")
|
|
||||||
print("\tcheck_services")
|
|
||||||
print("\tadd_news")
|
|
||||||
print("\tdel_news")
|
|
||||||
print("\tlogs")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
url = getenv(API_URL_ENV)
|
for command in self.commands.keys():
|
||||||
|
print("\t%s" % command)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
url = getenv(self.api_url_env)
|
||||||
valid_cmd = False
|
valid_cmd = False
|
||||||
|
|
||||||
for cmd in commands:
|
if url is None:
|
||||||
|
self.log.error(
|
||||||
|
"Please specify the API URL using %s environment variable"
|
||||||
|
% self.api_url_env
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
for cmd in self.commands:
|
||||||
if argv[1] == cmd:
|
if argv[1] == cmd:
|
||||||
valid_cmd = True
|
valid_cmd = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if not valid_cmd:
|
if not valid_cmd:
|
||||||
log.error(
|
self.log.error(
|
||||||
"Invalid command, run the script with no commands to list the available commands"
|
"Invalid command, run the script with no commands to list the available commands"
|
||||||
)
|
)
|
||||||
exit(1)
|
return False
|
||||||
|
|
||||||
if url is None:
|
|
||||||
log.error(
|
|
||||||
"Please specify the API URL using %s environment variable" % API_URL_ENV
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
password = log.password("Please enter the admin password")
|
password = self.log.password("Please enter the admin password")
|
||||||
api = AdminAPI(url, password)
|
self.api = AdminAPI(url, password)
|
||||||
|
|
||||||
if len(argv) == 2:
|
if len(argv) == 2:
|
||||||
__handle_command(log, api, argv[1])
|
self.handle_command(argv[1])
|
||||||
|
|
||||||
elif len(argv) == 3:
|
elif len(argv) == 3:
|
||||||
__handle_command_with_file(log, api, argv[1], argv[2])
|
self.handle_command(argv[1], argv[2])
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print()
|
self.log.error("Command cancelled")
|
||||||
log.error("Command cancelled")
|
return False
|
||||||
exit(1)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("Command failed: %s" % e)
|
self.log.error("Command failed: %s" % e)
|
||||||
exit(1)
|
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
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
script = AdminScript()
|
||||||
|
exit(script.run() if 1 else 0)
|
||||||
|
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"
|
||||||
|
}
|
@ -2,18 +2,25 @@ FROM golang:1.23.4
|
|||||||
|
|
||||||
WORKDIR /api
|
WORKDIR /api
|
||||||
|
|
||||||
COPY *.go ./
|
RUN useradd runner -r -u 1001 -d /api
|
||||||
|
RUN chown -R runner:runner /api
|
||||||
|
USER runner
|
||||||
|
|
||||||
COPY *.mod ./
|
COPY *.mod ./
|
||||||
COPY *.sum ./
|
COPY *.sum ./
|
||||||
|
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY *.go ./
|
||||||
COPY Makefile ./
|
COPY Makefile ./
|
||||||
|
COPY config ./config
|
||||||
|
COPY database ./database
|
||||||
|
COPY routes ./routes
|
||||||
|
COPY sql ./sql
|
||||||
|
COPY status ./status
|
||||||
COPY util ./util
|
COPY util ./util
|
||||||
COPY views ./views
|
COPY views ./views
|
||||||
COPY routes ./routes
|
|
||||||
COPY config ./config
|
|
||||||
COPY status ./status
|
|
||||||
COPY database ./database
|
|
||||||
|
|
||||||
EXPOSE 7001
|
|
||||||
RUN make
|
RUN make
|
||||||
|
|
||||||
ENTRYPOINT ["/api/api.elf"]
|
ENTRYPOINT ["/api/api.elf"]
|
||||||
|
@ -6,7 +6,7 @@ api.elf: $(GOSRCS)
|
|||||||
go build -o $@
|
go build -o $@
|
||||||
|
|
||||||
run:
|
run:
|
||||||
API_DEBUG=true API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./api.elf
|
WEBSITE_DEBUG=true WEBSITE_PASSWORD=test ./api.elf
|
||||||
|
|
||||||
format:
|
format:
|
||||||
gofmt -s -w .
|
gofmt -s -w .
|
||||||
|
@ -38,13 +38,9 @@ func (c *Type) Load() (err error) {
|
|||||||
// default options
|
// default options
|
||||||
c.Options = []Option{
|
c.Options = []Option{
|
||||||
{Name: "debug", Value: "false", Type: OPTION_TYPE_BOOL, Required: true}, // should display debug messgaes?
|
{Name: "debug", Value: "false", Type: OPTION_TYPE_BOOL, Required: true}, // should display debug messgaes?
|
||||||
{Name: "index", Value: "true", Type: OPTION_TYPE_BOOL, Required: false}, // should display the index page (view/index.md)?
|
{Name: "app_url", Value: "http://localhost:7001/", Type: OPTION_TYPE_URL, Required: true}, // frontend application URL for the website
|
||||||
|
|
||||||
{Name: "api_url", Value: "http://localhost:7001/", Type: OPTION_TYPE_URL, Required: true}, // API URL for the website
|
|
||||||
{Name: "frontend_url", Value: "http://localhost:5173/", Type: OPTION_TYPE_URL, Required: true}, // frontend application URL for the website
|
|
||||||
|
|
||||||
{Name: "password", Value: "", Type: OPTION_TYPE_STR, Required: true}, // admin password
|
{Name: "password", Value: "", Type: OPTION_TYPE_STR, Required: true}, // admin password
|
||||||
{Name: "host", Value: "0.0.0.0:7001", Type: OPTION_TYPE_STR, Required: true}, // host the server should listen on
|
{Name: "host", Value: "0.0.0.0:7002", Type: OPTION_TYPE_STR, Required: true}, // host the server should listen on
|
||||||
{Name: "ip_header", Value: "X-Real-IP", Type: OPTION_TYPE_STR, Required: false}, // header that should be checked for obtaining the client IP
|
{Name: "ip_header", Value: "X-Real-IP", Type: OPTION_TYPE_STR, Required: false}, // header that should be checked for obtaining the client IP
|
||||||
{Name: "interval", Value: "1h", Type: OPTION_TYPE_STR, Required: false}, // service status check interval
|
{Name: "interval", Value: "1h", Type: OPTION_TYPE_STR, Required: false}, // service status check interval
|
||||||
{Name: "timeout", Value: "15s", Type: OPTION_TYPE_STR, Required: false}, // timeout for the service status check
|
{Name: "timeout", Value: "15s", Type: OPTION_TYPE_STR, Required: false}, // timeout for the service status check
|
||||||
|
@ -25,7 +25,7 @@ type Option struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *Option) Env() string {
|
func (o *Option) Env() string {
|
||||||
return strings.ToUpper(fmt.Sprintf("API_%s", o.Name))
|
return strings.ToUpper(fmt.Sprintf("WEBSITE_%s", o.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Option) Load() (err error) {
|
func (o *Option) Load() (err error) {
|
||||||
|
@ -24,8 +24,8 @@ func (db *Type) AdminLogNext(l *AdminLog) bool {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if nil == db.rows {
|
if nil == db.rows {
|
||||||
if db.rows, err = db.sql.Query("SELECT * FROM admin_log"); err != nil {
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_ADMIN_LOG); err != nil {
|
||||||
util.Fail("failed to query admin_log table: %s", err.Error())
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ func (db *Type) AdminLogNext(l *AdminLog) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = l.Scan(db.rows); err != nil {
|
if err = l.Scan(db.rows); err != nil {
|
||||||
util.Fail("failed to scan the admin_log table: %s", err.Error())
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ fail:
|
|||||||
|
|
||||||
func (db *Type) AdminLogAdd(l *AdminLog) error {
|
func (db *Type) AdminLogAdd(l *AdminLog) error {
|
||||||
_, err := db.sql.Exec(
|
_, err := db.sql.Exec(
|
||||||
`INSERT INTO admin_log(
|
"INSERT INTO "+TABLE_ADMIN_LOG+`(
|
||||||
action, time
|
action, time
|
||||||
) values(?, ?)`,
|
) values(?, ?)`,
|
||||||
&l.Action, &l.Time,
|
&l.Action, &l.Time,
|
@ -2,64 +2,62 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SQL_PATH = "sql"
|
||||||
|
|
||||||
|
TABLE_ADMIN_LOG = "admin_log" // stores administrator logs
|
||||||
|
TABLE_METRICS = "metrics" // stores API usage metrcis
|
||||||
|
TABLE_NEWS = "news" // stores news posts
|
||||||
|
TABLE_SERVICES = "services" // stores services
|
||||||
|
TABLE_PROJECTS = "projects" // stores projects
|
||||||
|
)
|
||||||
|
|
||||||
|
var tables []string = []string{
|
||||||
|
TABLE_ADMIN_LOG, TABLE_METRICS, TABLE_NEWS,
|
||||||
|
TABLE_SERVICES, TABLE_PROJECTS,
|
||||||
|
}
|
||||||
|
|
||||||
type Type struct {
|
type Type struct {
|
||||||
sql *sql.DB
|
sql *sql.DB
|
||||||
rows *sql.Rows
|
rows *sql.Rows
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Type) Load() (err error) {
|
func (db *Type) create_table(table string) error {
|
||||||
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
var (
|
||||||
return fmt.Errorf("cannot access the database: %s", err.Error())
|
err error
|
||||||
|
query []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
query_path := path.Join(SQL_PATH, table+".sql")
|
||||||
|
|
||||||
|
if query, err = os.ReadFile(query_path); err != nil {
|
||||||
|
return fmt.Errorf("failed to read %s for table %s: %", query_path, table, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// see database/service.go
|
if _, err = db.sql.Exec(string(query)); err != nil {
|
||||||
_, err = db.sql.Exec(`
|
return fmt.Errorf("failed to create the %s table: %s", table, err.Error())
|
||||||
CREATE TABLE IF NOT EXISTS services(
|
}
|
||||||
name TEXT NOT NULL UNIQUE,
|
|
||||||
desc TEXT NOT NULL,
|
return nil
|
||||||
check_time INTEGER NOT NULL,
|
}
|
||||||
check_res INTEGER NOT NULL,
|
|
||||||
check_url TEXT NOT NULL,
|
func (db *Type) Load() (err error) {
|
||||||
clear TEXT,
|
if db.sql, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||||
onion TEXT,
|
return fmt.Errorf("failed access the database: %s", err.Error())
|
||||||
i2p TEXT
|
}
|
||||||
);
|
|
||||||
`)
|
for _, table := range tables {
|
||||||
|
if err = db.create_table(table); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return fmt.Errorf("failed to create the services table: %s", err.Error())
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// see database/news.go
|
|
||||||
_, err = db.sql.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS news(
|
|
||||||
id TEXT NOT NULL UNIQUE,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
author TEXT NOT NULL,
|
|
||||||
time INTEGER NOT NULL,
|
|
||||||
content TEXT NOT NULL
|
|
||||||
);
|
|
||||||
`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create the news table: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// see database/admin.go
|
|
||||||
_, err = db.sql.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS admin_log(
|
|
||||||
action TEXT NOT NULL,
|
|
||||||
time INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create the admin_log table: %s", err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
@ -64,8 +64,8 @@ func (db *Type) NewsNext(n *News) bool {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if nil == db.rows {
|
if nil == db.rows {
|
||||||
if db.rows, err = db.sql.Query("SELECT * FROM news"); err != nil {
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_NEWS); err != nil {
|
||||||
util.Fail("failed to query news table: %s", err.Error())
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ func (db *Type) NewsNext(n *News) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = n.Scan(db.rows); err != nil {
|
if err = n.Scan(db.rows); err != nil {
|
||||||
util.Fail("failed to scan the news table: %s", err.Error())
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ fail:
|
|||||||
|
|
||||||
func (db *Type) NewsRemove(id string) error {
|
func (db *Type) NewsRemove(id string) error {
|
||||||
_, err := db.sql.Exec(
|
_, err := db.sql.Exec(
|
||||||
"DELETE FROM news WHERE id = ?",
|
"DELETE FROM "+TABLE_NEWS+" WHERE id = ?",
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ func (db *Type) NewsAdd(n *News) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.sql.Exec(
|
_, err = db.sql.Exec(
|
||||||
`INSERT OR REPLACE INTO news(
|
"INSERT OR REPLACE INTO "+TABLE_NEWS+`(
|
||||||
id, title, author, time, content
|
id, title, author, time, content
|
||||||
) values(?, ?, ?, ?, ?)`,
|
) values(?, ?, ?, ?, ?)`,
|
||||||
n.ID, n.title,
|
n.ID, n.title,
|
||||||
|
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
|
||||||
|
}
|
@ -58,8 +58,8 @@ func (db *Type) ServiceNext(s *Service) bool {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if nil == db.rows {
|
if nil == db.rows {
|
||||||
if db.rows, err = db.sql.Query("SELECT * FROM services"); err != nil {
|
if db.rows, err = db.sql.Query("SELECT * FROM " + TABLE_SERVICES); err != nil {
|
||||||
util.Fail("failed to query services table: %s", err.Error())
|
util.Fail("failed to query table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,7 +69,7 @@ func (db *Type) ServiceNext(s *Service) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = s.Scan(db.rows, nil); err != nil {
|
if err = s.Scan(db.rows, nil); err != nil {
|
||||||
util.Fail("failed to scan the services table: %s", err.Error())
|
util.Fail("failed to scan the table: %s", err.Error())
|
||||||
goto fail
|
goto fail
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ func (db *Type) ServiceFind(name string) (*Service, error) {
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if row = db.sql.QueryRow("SELECT * FROM services WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows {
|
if row = db.sql.QueryRow("SELECT * FROM "+TABLE_SERVICES+" WHERE name = ?", name); row == nil || row.Err() == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ func (db *Type) ServiceFind(name string) (*Service, error) {
|
|||||||
|
|
||||||
func (db *Type) ServiceRemove(name string) error {
|
func (db *Type) ServiceRemove(name string) error {
|
||||||
_, err := db.sql.Exec(
|
_, err := db.sql.Exec(
|
||||||
"DELETE FROM services WHERE name = ?",
|
"DELETE FROM "+TABLE_SERVICES+" WHERE name = ?",
|
||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ func (db *Type) ServiceUpdate(s *Service) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.sql.Exec(
|
_, err = db.sql.Exec(
|
||||||
`INSERT OR REPLACE INTO services(
|
"INSERT OR REPLACE INTO "+TABLE_SERVICES+`(
|
||||||
name, desc, check_time, check_res, check_url, clear, onion, i2p
|
name, desc, check_time, check_res, check_url, clear, onion, i2p
|
||||||
) values(?, ?, ?, ?, ?, ?, ?, ?)`,
|
) values(?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
s.Name, s.desc,
|
s.Name, s.desc,
|
||||||
|
@ -5,7 +5,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/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -17,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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 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=
|
||||||
|
@ -89,14 +89,21 @@ func main() {
|
|||||||
|
|
||||||
// v1 user routes
|
// v1 user routes
|
||||||
v1.Get("/services", routes.GET_Services)
|
v1.Get("/services", routes.GET_Services)
|
||||||
|
v1.Get("/projects", routes.GET_Projects)
|
||||||
|
v1.Get("/metrics", routes.GET_Metrics)
|
||||||
v1.Get("/news/:lang", routes.GET_News)
|
v1.Get("/news/:lang", routes.GET_News)
|
||||||
|
|
||||||
// v1 admin routes
|
// v1 admin routes
|
||||||
v1.Use("/admin", routes.AuthMiddleware)
|
v1.Use("/admin", routes.AuthMiddleware)
|
||||||
v1.Get("/admin/logs", routes.GET_AdminLogs)
|
v1.Get("/admin/logs", routes.GET_AdminLogs)
|
||||||
|
|
||||||
v1.Get("/admin/service/check", routes.GET_CheckService)
|
v1.Get("/admin/service/check", routes.GET_CheckService)
|
||||||
v1.Put("/admin/service/add", routes.PUT_AddService)
|
v1.Put("/admin/service/add", routes.PUT_AddService)
|
||||||
v1.Delete("/admin/service/del", routes.DEL_DelService)
|
v1.Delete("/admin/service/del", routes.DEL_DelService)
|
||||||
|
|
||||||
|
v1.Put("/admin/project/add", routes.PUT_AddProject)
|
||||||
|
v1.Delete("/admin/project/del", routes.DEL_DelProject)
|
||||||
|
|
||||||
v1.Put("/admin/news/add", routes.PUT_AddNews)
|
v1.Put("/admin/news/add", routes.PUT_AddNews)
|
||||||
v1.Delete("/admin/news/del", routes.DEL_DelNews)
|
v1.Delete("/admin/news/del", routes.DEL_DelNews)
|
||||||
|
|
||||||
|
@ -103,6 +103,56 @@ func GET_CheckService(c *fiber.Ctx) error {
|
|||||||
return util.JSON(c, 200, nil)
|
return util.JSON(c, 200, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PUT_AddProject(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
project database.Project
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
if c.BodyParser(&project) != nil {
|
||||||
|
return util.ErrBadJSON(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !project.IsValid() {
|
||||||
|
return util.ErrBadReq(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = admin_log(c, fmt.Sprintf("Added project \"%s\"", project.Name)); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.ProjectAdd(&project); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DEL_DelProject(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db := c.Locals("database").(*database.Type)
|
||||||
|
|
||||||
|
if name = c.Query("name"); name == "" {
|
||||||
|
util.ErrBadReq(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = admin_log(c, fmt.Sprintf("Removed project \"%s\"", name)); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.ProjectRemove(name); err != nil {
|
||||||
|
return util.ErrInternal(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSON(c, 200, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func DEL_DelNews(c *fiber.Ctx) error {
|
func DEL_DelNews(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
id string
|
id string
|
||||||
|
@ -3,30 +3,12 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"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/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GET_Index(c *fiber.Ctx) error {
|
func GET_Index(c *fiber.Ctx) error {
|
||||||
var (
|
|
||||||
md []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
conf := c.Locals("config").(*config.Type)
|
conf := c.Locals("config").(*config.Type)
|
||||||
|
app := conf.GetURL("app_url")
|
||||||
|
|
||||||
if !conf.GetBool("index") {
|
// redirect to the API documentation
|
||||||
return util.ErrNotFound(c)
|
return c.Redirect(app.JoinPath("/doc/api").String())
|
||||||
}
|
|
||||||
|
|
||||||
frontend := conf.GetURL("frontend_url")
|
|
||||||
api := conf.GetURL("api_url")
|
|
||||||
|
|
||||||
if md, err = util.Render("views/index.md", fiber.Map{
|
|
||||||
"frontend": frontend,
|
|
||||||
"api": api,
|
|
||||||
}); err != nil {
|
|
||||||
return util.ErrInternal(c, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.Markdown(c, md)
|
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
@ -40,7 +40,7 @@ func GET_News(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
db := c.Locals("database").(*database.Type)
|
db := c.Locals("database").(*database.Type)
|
||||||
conf := c.Locals("config").(*config.Type)
|
conf := c.Locals("config").(*config.Type)
|
||||||
frontend := conf.GetURL("frontend_url")
|
app := conf.GetURL("app_url")
|
||||||
lang := c.Params("lang")
|
lang := c.Params("lang")
|
||||||
|
|
||||||
if lang == "" || len(lang) != 2 {
|
if lang == "" || len(lang) != 2 {
|
||||||
@ -63,14 +63,16 @@ func GET_News(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if feed, err = util.Render("views/news.xml", fiber.Map{
|
if feed, err = util.Render("views/news.xml", fiber.Map{
|
||||||
"frontend": frontend,
|
|
||||||
"updated": time.Now().Format(time.RFC3339),
|
"updated": time.Now().Format(time.RFC3339),
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
"lang": lang,
|
"lang": lang,
|
||||||
|
"app": app,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return util.ErrInternal(c, err)
|
return util.ErrInternal(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Set("Content-Disposition", "attachment; filename=\"news.atom\"")
|
||||||
c.Set("Content-Type", "application/atom+xml; charset=utf-8")
|
c.Set("Content-Type", "application/atom+xml; charset=utf-8")
|
||||||
|
|
||||||
return c.Send(feed)
|
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,
|
||||||
|
})
|
||||||
|
}
|
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
|
||||||
|
);
|
@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/ngn13/website/api/config"
|
"github.com/ngn13/website/api/config"
|
||||||
"github.com/russross/blackfriday/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func IP(c *fiber.Ctx) string {
|
func IP(c *fiber.Ctx) string {
|
||||||
@ -20,15 +19,6 @@ func IP(c *fiber.Ctx) string {
|
|||||||
return c.IP()
|
return c.IP()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Markdown(c *fiber.Ctx, raw []byte) error {
|
|
||||||
exts := blackfriday.FencedCode
|
|
||||||
exts |= blackfriday.NoEmptyLineBeforeBlock
|
|
||||||
exts |= blackfriday.HardLineBreak
|
|
||||||
|
|
||||||
c.Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
return c.Send(blackfriday.Run(raw, blackfriday.WithExtensions(exts)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func JSON(c *fiber.Ctx, code int, data fiber.Map) error {
|
func JSON(c *fiber.Ctx, code int, data fiber.Map) error {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
data = fiber.Map{}
|
data = fiber.Map{}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
|
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
<title>{{.frontend.Host}} news</title>
|
<title>{{.app.Host}} news</title>
|
||||||
<updated>{{.updated}}</updated>
|
<updated>{{.updated}}</updated>
|
||||||
<subtitle>News and updates about my projects and self-hosted services</subtitle>
|
<subtitle>News and updates about my projects and self-hosted services</subtitle>
|
||||||
<link href="{{.frontend.JoinPath "/news"}}"></link>
|
<link href="{{.app.JoinPath "/news"}}"></link>
|
||||||
{{ range .entries }}
|
{{ range .entries }}
|
||||||
<entry>
|
<entry>
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.svelte-kit
|
.svelte-kit
|
||||||
build
|
|
||||||
public
|
public
|
||||||
|
build
|
||||||
|
@ -1,14 +1,25 @@
|
|||||||
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
|
||||||
|
|
||||||
@ -16,8 +27,13 @@ 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
|
14
app/package-lock.json
generated
14
app/package-lock.json
generated
@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "5.0.0",
|
"version": "6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "5.0.0",
|
"version": "6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dompurify": "^3.2.0",
|
"@types/dompurify": "^3.2.0",
|
||||||
"dompurify": "^3.2.3",
|
"dompurify": "^3.2.3",
|
||||||
"marked": "^15.0.4",
|
"marked": "^15.0.6",
|
||||||
"svelte-i18n": "^4.0.1"
|
"svelte-i18n": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -1093,6 +1093,7 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
|
||||||
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
|
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@types/trusted-types": "^2.0.7"
|
"@types/trusted-types": "^2.0.7"
|
||||||
}
|
}
|
||||||
@ -1399,9 +1400,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "15.0.4",
|
"version": "15.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz",
|
||||||
"integrity": "sha512-TCHvDqmb3ZJ4PWG7VEGVgtefA5/euFmsIhxtD0XsBxI39gUSKL81mIRFdt0AiNQozUahd4ke98ZdirExd/vSEw==",
|
"integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==",
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
"version": "6.0",
|
"version": "6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "VITE_API_URL=http://127.0.0.1:7001 VITE_FRONTEND_URL=http://localhost:5173 vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview --host",
|
"preview": "vite preview",
|
||||||
"lint": "prettier --check .",
|
"lint": "prettier --check .",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
@ -21,9 +21,8 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/dompurify": "^3.2.0",
|
|
||||||
"dompurify": "^3.2.3",
|
"dompurify": "^3.2.3",
|
||||||
"marked": "^15.0.4",
|
"marked": "^15.0.6",
|
||||||
"svelte-i18n": "^4.0.1"
|
"svelte-i18n": "^4.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { locale } from "svelte-i18n";
|
|
||||||
|
|
||||||
export const handle = async ({ event, resolve }) => {
|
|
||||||
const lang = event.request.headers.get("accept-language")?.split(",")[0];
|
|
||||||
if (lang) locale.set(lang);
|
|
||||||
return resolve(event);
|
|
||||||
};
|
|
@ -1,21 +1,37 @@
|
|||||||
const version = "v1";
|
import { urljoin } from "$lib/util.js";
|
||||||
const url = new URL(version + "/", import.meta.env.VITE_API_URL).href;
|
|
||||||
|
|
||||||
function join(path) {
|
const api_version = "v1";
|
||||||
if (null === path || path === "") return url;
|
const api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version);
|
||||||
|
|
||||||
if (path[0] === "/") path = path.slice(1);
|
function api_urljoin(path = null, query = {}) {
|
||||||
|
return urljoin(api_url, path, query);
|
||||||
return new URL(path, url).href;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function services(fetch) {
|
function api_check_err(json) {
|
||||||
const res = await fetch(join("/services"));
|
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();
|
const json = await res.json();
|
||||||
|
api_check_err(json);
|
||||||
if (!("result" in json)) return [];
|
return json["result"];
|
||||||
|
|
||||||
return json.result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { version, join, services };
|
async function api_get_metrics(fetch) {
|
||||||
|
return await api_http_get(fetch, api_urljoin("/metrics"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api_get_services(fetch) {
|
||||||
|
return await api_http_get(fetch, api_urljoin("/services"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api_get_projects(fetch) {
|
||||||
|
return await api_http_get(fetch, api_urljoin("/projects"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { api_version, api_urljoin, api_get_metrics, api_get_services, api_get_projects };
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { click } from "$lib/util.js";
|
|
||||||
export let title;
|
|
||||||
export let url;
|
|
||||||
|
|
||||||
let current = "";
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while (title.length > i) {
|
|
||||||
let c = title[i];
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
current += c;
|
|
||||||
},
|
|
||||||
100 * (i + 1)
|
|
||||||
);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<a on:click={click} data-sveltekit-preload-data href={url}>
|
|
||||||
<div class="title">
|
|
||||||
{current}
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
a {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--dark-three);
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.4s;
|
|
||||||
text-decoration: none;
|
|
||||||
border: solid 1px var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover > .title {
|
|
||||||
text-shadow: var(--text-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
border: solid 1px var(--dark-two);
|
|
||||||
background: var(--dark-two);
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: 7px 7px 0px 0px;
|
|
||||||
font-size: 20px;
|
|
||||||
font-family:
|
|
||||||
Consolas,
|
|
||||||
Monaco,
|
|
||||||
Lucida Console,
|
|
||||||
Liberation Mono,
|
|
||||||
DejaVu Sans Mono,
|
|
||||||
Bitstream Vera Sans Mono,
|
|
||||||
Courier New,
|
|
||||||
monospace;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
background: var(--dark-three);
|
|
||||||
padding: 30px;
|
|
||||||
padding-top: 30px;
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,25 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let class_name;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<main class={class_name}>
|
|
||||||
<slot></slot>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
main {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
padding: 50px;
|
|
||||||
gap: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 900px) {
|
|
||||||
main {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
28
app/src/lib/doc.js
Normal file
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>
|
@ -1,28 +1,35 @@
|
|||||||
<script>
|
<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 Link from "$lib/link.svelte";
|
||||||
import { color } from "$lib/util.js";
|
|
||||||
|
import { onMount } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
let visitor_count = 1001;
|
let data = {};
|
||||||
|
|
||||||
function should_congrat() {
|
onMount(async () => {
|
||||||
return visitor_count % 1000 == 0;
|
data = await api_get_metrics(fetch);
|
||||||
}
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer style="border-top: solid 2px var(--{color()});">
|
<footer style="border-top: solid 2px var(--{color()});">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<span>
|
<span>
|
||||||
<Link href="/" bold={true}>{$_("footer.source")}</Link>
|
<Link link={import.meta.env.WEBSITE_SOURCE_URL} bold={true}>{$_("footer.source")}</Link>
|
||||||
</span>
|
</span>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span>
|
<span>
|
||||||
<Link href="/" bold={true}>{$_("footer.license")}</Link>
|
<Link link={urljoin(import.meta.env.WEBSITE_APP_URL, "doc/license")} bold={true}
|
||||||
|
>{$_("footer.license")}</Link
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span>
|
<span>
|
||||||
<Link href="/" bold={true}>{$_("footer.privacy")}</Link>
|
<Link link={urljoin(import.meta.env.WEBSITE_APP_URL, "doc/privacy")} bold={true}
|
||||||
|
>{$_("footer.privacy")}</Link
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
@ -31,9 +38,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="useless">
|
<div class="useless">
|
||||||
<span>
|
<span>
|
||||||
{$_("footer.number", { values: { count: visitor_count } })}
|
{$_("footer.number", {
|
||||||
{#if should_congrat()}
|
values: {
|
||||||
<span style="color: var(--{color()})">({$_("footer.congrats")})</span>
|
total: data.total,
|
||||||
|
since: date_from_ts(data.since),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
{#if data.number % 1000 == 0}
|
||||||
|
<span style="color: var(--{color()})">({$_("footer.wow")})</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { frontend_url, api_url } from "$lib/util.js";
|
import { api_urljoin } from "$lib/api.js";
|
||||||
|
import { app_url } from "$lib/util.js";
|
||||||
|
|
||||||
export let desc, title;
|
export let desc, title;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -8,8 +10,13 @@
|
|||||||
|
|
||||||
<meta content="[ngn.tf] | {title}" property="og:title" />
|
<meta content="[ngn.tf] | {title}" property="og:title" />
|
||||||
<meta content={desc} property="og:description" />
|
<meta content={desc} property="og:description" />
|
||||||
<meta content={frontend_url()} property="og:url" />
|
<meta content={app_url()} property="og:url" />
|
||||||
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
||||||
|
|
||||||
<link rel="alternate" type="application/atom+xml" href={api_url("/news/en")} title="Atom Feed" />
|
<link
|
||||||
|
rel="alternate"
|
||||||
|
type="application/atom+xml"
|
||||||
|
href={api_urljoin("/news/en")}
|
||||||
|
title="Atom Feed"
|
||||||
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
@ -4,29 +4,21 @@
|
|||||||
|
|
||||||
export let picture = "";
|
export let picture = "";
|
||||||
export let title = "";
|
export let title = "";
|
||||||
|
|
||||||
let current = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < title.length; i++) {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
current += title[i];
|
|
||||||
},
|
|
||||||
100 * (i + 1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1 style="color: var(--{color()})">{current}</h1>
|
<div>
|
||||||
|
<h1 class="title" style="color: var(--{color()})">{title.toLowerCase()}</h1>
|
||||||
|
<h1 class="cursor" style="color: var(--{color()})">_</h1>
|
||||||
|
</div>
|
||||||
<img src="/profile/{picture}.png" alt="" />
|
<img src="/profile/{picture}.png" alt="" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
header {
|
header {
|
||||||
background: linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96)),
|
background: var(--background);
|
||||||
url("https://files.ngn.tf/banner.png");
|
|
||||||
background-size: 50%;
|
background-size: 50%;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -35,15 +27,12 @@
|
|||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
header div {
|
||||||
padding: 50px 50px 0 50px;
|
display: flex;
|
||||||
width: 220px;
|
flex-direction: row;
|
||||||
bottom: 0;
|
align-items: end;
|
||||||
left: 0;
|
padding: 50px 50px 30px 50px;
|
||||||
}
|
font-size: var(--size-6);
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: var(--size-7);
|
|
||||||
font-family:
|
font-family:
|
||||||
Consolas,
|
Consolas,
|
||||||
Monaco,
|
Monaco,
|
||||||
@ -53,29 +42,38 @@
|
|||||||
Bitstream Vera Sans Mono,
|
Bitstream Vera Sans Mono,
|
||||||
Courier New,
|
Courier New,
|
||||||
monospace;
|
monospace;
|
||||||
padding: 50px 50px 30px 50px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: center;
|
justify-content: start;
|
||||||
color: white;
|
width: min-content;
|
||||||
text-shadow: var(--text-shadow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1::after {
|
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: "_";
|
content: "_";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
animation: blink 1.5s steps(2) infinite;
|
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) {
|
@media only screen and (max-width: 900px) {
|
||||||
header {
|
header {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
header img {
|
||||||
padding: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
export default [
|
|
||||||
{ code: "en", name: "English", icon: "🇬🇧", path: "../locales/en.json" },
|
|
||||||
{ code: "tr", name: "Turkish", icon: "🇹🇷", path: "../locales/tr.json" },
|
|
||||||
];
|
|
@ -5,10 +5,13 @@
|
|||||||
const default_color = "white-1";
|
const default_color = "white-1";
|
||||||
|
|
||||||
export let active = false;
|
export let active = false;
|
||||||
|
export let highlight = true;
|
||||||
export let link = "";
|
export let link = "";
|
||||||
export let icon = "";
|
export let icon = "";
|
||||||
|
|
||||||
let style = `text-decoration-color: var(--${color()});`;
|
let style = "";
|
||||||
|
|
||||||
|
if (highlight) style = `text-decoration-color: var(--${color()});`;
|
||||||
|
|
||||||
if (active) style += `color: var(--${color()});`;
|
if (active) style += `color: var(--${color()});`;
|
||||||
else style += `color: var(--${default_color});`;
|
else style += `color: var(--${default_color});`;
|
||||||
@ -17,6 +20,18 @@
|
|||||||
{#if icon != ""}
|
{#if icon != ""}
|
||||||
<Icon {icon} />
|
<Icon {icon} />
|
||||||
{/if}
|
{/if}
|
||||||
<a {style} href={link}>
|
{#if highlight}
|
||||||
|
<a data-sveltekit-preload-data {style} href={link}>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</a>
|
</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,
|
||||||
|
};
|
@ -18,13 +18,13 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
nav {
|
nav {
|
||||||
|
box-shadow: var(--box-shadow-1);
|
||||||
background: var(--black-1);
|
background: var(--black-1);
|
||||||
padding: 20px 30px 20px 20px;
|
padding: 20px 30px 20px 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-shadow: var(--def-shadow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
<script>
|
<script>
|
||||||
import { locale } from "svelte-i18n";
|
import { locale_list, locale_select, locale_index } from "$lib/locale.js";
|
||||||
import languages from "$lib/lang.js";
|
|
||||||
let icon = "",
|
let len = locale_list.length;
|
||||||
indx = 0,
|
|
||||||
len = languages.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() {
|
function next() {
|
||||||
if (indx >= languages.length) indx = 0;
|
locale_select(get_next($locale_index).code);
|
||||||
|
|
||||||
icon = languages[indx].icon;
|
|
||||||
locale.set(languages[indx++].code);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (indx = 0; indx < len; indx++) {
|
|
||||||
if (languages[indx].code == $locale.slice(0, 2)) {
|
|
||||||
icon = languages[indx++].icon;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button on:click={next}>{icon}</button>
|
<button on:click={next}>
|
||||||
|
{get_next($locale_index).icon}
|
||||||
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
button {
|
button {
|
||||||
|
@ -3,38 +3,47 @@
|
|||||||
import Link from "$lib/link.svelte";
|
import Link from "$lib/link.svelte";
|
||||||
|
|
||||||
import { color, time_from_ts } from "$lib/util.js";
|
import { color, time_from_ts } from "$lib/util.js";
|
||||||
import { locale } from "svelte-i18n";
|
import { locale, _ } from "svelte-i18n";
|
||||||
|
|
||||||
export let service = {};
|
export let service = {};
|
||||||
let style = "";
|
|
||||||
|
|
||||||
if (service.check_res == 0) style = "opacity: 70%";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main {style}>
|
<main>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>{service.name}</h1>
|
<h1>{service.name}</h1>
|
||||||
<p>{service.desc[$locale.slice(0, 2)]}</p>
|
<p>{service.desc[$locale]}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<Link link={service.clear}><Icon icon="nf-oct-link" /></Link>
|
<Link highlight={false} link={service.clear}><Icon icon="nf-oct-link" /></Link>
|
||||||
{#if service.onion != ""}
|
{#if service.onion != ""}
|
||||||
<Link link={service.onion}><Icon icon="nf-linux-tor" /></Link>
|
<Link highlight={false} link={service.onion}><Icon icon="nf-linux-tor" /></Link>
|
||||||
{/if}
|
{/if}
|
||||||
{#if service.i2p != ""}
|
{#if service.i2p != ""}
|
||||||
<Link link={service.i2p}><span style="color: var(--{color()})">I2P</span></Link>
|
<Link highlight={false} link={service.i2p}
|
||||||
|
><span style="color: var(--{color()})">I2P</span></Link
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="check">
|
<div class="check">
|
||||||
<h1>Last checked at {time_from_ts(service.check_time)}</h1>
|
<h1>
|
||||||
|
{$_("services.last", {
|
||||||
|
values: { time: time_from_ts(service.check_time) },
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
{#if service.check_res == 0}
|
{#if service.check_res == 0}
|
||||||
<span style="background: var(--white-2)">Down</span>
|
<span style="background: var(--white-2)">
|
||||||
|
{$_("services.status.down")}
|
||||||
|
</span>
|
||||||
{:else if service.check_res == 1}
|
{:else if service.check_res == 1}
|
||||||
<span style="background: var(--{color()})">Up</span>
|
<span style="background: var(--{color()})">
|
||||||
|
{$_("services.status.up")}
|
||||||
|
</span>
|
||||||
{:else if service.check_res == 2}
|
{:else if service.check_res == 2}
|
||||||
<span style="background: var(--white-2)">Slow</span>
|
<span style="background: var(--{color()}); filter: brightness(50%);">
|
||||||
|
{$_("services.status.slow")}
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@ -58,16 +67,19 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: var(--white-1);
|
color: var(--white-1);
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
main .info .title h1 {
|
main .info .title h1 {
|
||||||
font-size: var(--size-5);
|
font-size: var(--size-5);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
main .info .title p {
|
main .info .title p {
|
||||||
font-size: var(--size-4);
|
font-size: var(--size-4);
|
||||||
color: var(--white-2);
|
color: var(--white-2);
|
||||||
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
main .info .links {
|
main .info .links {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { join } from "$lib/api.js";
|
import { locale_from_browser } from "$lib/locale.js";
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
"yellow",
|
"yellow",
|
||||||
@ -8,8 +8,8 @@ const colors = [
|
|||||||
"red",
|
"red",
|
||||||
// "blue" (looks kinda ass)
|
// "blue" (looks kinda ass)
|
||||||
];
|
];
|
||||||
|
|
||||||
let colors_pos = -1;
|
let colors_pos = -1;
|
||||||
let api_url = join;
|
|
||||||
|
|
||||||
function color() {
|
function color() {
|
||||||
if (colors_pos < 0) colors_pos = Math.floor(Math.random() * colors.length);
|
if (colors_pos < 0) colors_pos = Math.floor(Math.random() * colors.length);
|
||||||
@ -23,13 +23,49 @@ function click() {
|
|||||||
audio.play();
|
audio.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
function frontend_url(path) {
|
function urljoin(url, path = null, query = {}) {
|
||||||
if (null !== path && path !== "") return new URL(path, import.meta.env.VITE_FRONTEND_URL).href;
|
if (undefined === url || null === url) return;
|
||||||
else return new URL(import.meta.env.VITE_FRONTEND_URL).href;
|
|
||||||
|
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) {
|
function time_from_ts(ts) {
|
||||||
return new Date(ts * 1000).toLocaleTimeString();
|
if (ts === 0 || ts === undefined) return;
|
||||||
|
|
||||||
|
let ts_date = new Date(ts * 1000);
|
||||||
|
let ts_zone = ts_date.toString().match(/([A-Z]+[\+-][0-9]+)/)[1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
new Intl.DateTimeFormat(locale_from_browser(), {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
}).format(ts_date) + ` (${ts_zone})`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { api_url, frontend_url, click, color, time_from_ts };
|
function date_from_ts(ts) {
|
||||||
|
if (ts === 0 || ts === undefined) return;
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(locale_from_browser(), {
|
||||||
|
month: "2-digit",
|
||||||
|
year: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).format(new Date(ts * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { color, click, urljoin, app_url, time_from_ts, date_from_ts };
|
||||||
|
@ -2,15 +2,14 @@
|
|||||||
"navbar": {
|
"navbar": {
|
||||||
"home": "home",
|
"home": "home",
|
||||||
"services": "services",
|
"services": "services",
|
||||||
"news": "news",
|
|
||||||
"donate": "donate"
|
"donate": "donate"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "hello world!",
|
"title": "Hello world!",
|
||||||
"welcome": {
|
"welcome": {
|
||||||
"title": "about",
|
"title": "about",
|
||||||
"desc": "Welcome to my website, I'm ngn",
|
"desc": "Welcome to my website, I'm ngn",
|
||||||
"whoami": "I'm a privacy, security and freedom addvocate high-schooler from Turkey",
|
"whoami": "I'm a security, privacy and freedom advocate high-schooler from Turkey",
|
||||||
"interest": "I'm interested in system security and software development",
|
"interest": "I'm interested in system security and software development",
|
||||||
"support": "I love and support Free/Libre and Open Source Software (FLOSS)"
|
"support": "I love and support Free/Libre and Open Source Software (FLOSS)"
|
||||||
},
|
},
|
||||||
@ -18,6 +17,7 @@
|
|||||||
"title": "work",
|
"title": "work",
|
||||||
"desc": "I don't currently have a job, so I spend most of my time...",
|
"desc": "I don't currently have a job, so I spend most of my time...",
|
||||||
"build": "building stupid shit",
|
"build": "building stupid shit",
|
||||||
|
"fix": "fixing stupid shit",
|
||||||
"ctf": "solving CTF challenges",
|
"ctf": "solving CTF challenges",
|
||||||
"contribute": "contributing to random projects",
|
"contribute": "contributing to random projects",
|
||||||
"wiki": "expanding my wiki"
|
"wiki": "expanding my wiki"
|
||||||
@ -25,25 +25,59 @@
|
|||||||
"links": {
|
"links": {
|
||||||
"title": "contact",
|
"title": "contact",
|
||||||
"desc": "Here are some useful links if you want to get in contact with me",
|
"desc": "Here are some useful links if you want to get in contact with me",
|
||||||
"prefer": "preferred"
|
"prefer": "I highly prefer email, you can send encrypted emails using my PGP key"
|
||||||
},
|
},
|
||||||
"info": {
|
"services": {
|
||||||
"title": "services",
|
"title": "services",
|
||||||
"desc": "A part from working on stupid shit, I host free (as in freedom, and price) services available for all",
|
"desc": "A part from working on stupid shit, I host free (as in freedom, and price) services available for all",
|
||||||
"speed": "All of these services are available over a 600 Mbit/s interface",
|
"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",
|
"security": "All use SSL encrypted connection and they are all privacy-respecting",
|
||||||
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
|
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
|
||||||
"bullshit": "No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit",
|
"bullshit": "No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit",
|
||||||
"link": "see all the services"
|
"link": "See all the services!"
|
||||||
|
},
|
||||||
|
"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": {
|
"footer": {
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
"license": "License",
|
"license": "License",
|
||||||
"privacy": "Privacy",
|
"privacy": "Privacy",
|
||||||
"powered": "Powered by Svelte, Go, SQLite and donations",
|
"powered": "Powered by Svelte, Go, SQLite and donations",
|
||||||
"number": "You are the visitor number {count}",
|
"number": "Visited {total} times since {since}",
|
||||||
"congrat": "congrats!!",
|
"wow": "wow!!",
|
||||||
"version": "Using API version {api_version}, frontend version {frontend_version}"
|
"version": "Using API version {api_version}, frontend version {frontend_version}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
{
|
{
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"home": "anasayfa",
|
"home": "anasayfa",
|
||||||
"news": "haberler",
|
|
||||||
"services": "servisler",
|
"services": "servisler",
|
||||||
"language": "dil"
|
"donate": "bağış"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"title": "Merhaba Dünya!",
|
||||||
"welcome": {
|
"welcome": {
|
||||||
"title": "Websiteme hoşgeldiniz, ben ngn",
|
"title": "hakkımda",
|
||||||
|
"desc": "Websiteme hoşgeldiniz, ben ngn",
|
||||||
"whoami": "Türkiye'den, güvenlik, gizlik ve özgürlük savunucusu bir liseliyim",
|
"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",
|
"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"
|
"support": "Özgür/Libre ve Açık Kaynaklı Yazılımı (FLOSS) seviyorum ve destekliyorum"
|
||||||
},
|
},
|
||||||
"work": {
|
"work": {
|
||||||
"title": "Zamanım çoğunlukla şunlar ile geçiyor...",
|
"title": "iş",
|
||||||
|
"desc": "Şuan bir işim yok, o yüzden zamanımın çoğunu şunlarla geçiriyorum:",
|
||||||
"build": "salak şeyler inşa etmek",
|
"build": "salak şeyler inşa etmek",
|
||||||
"fix": "salak şeyleri düzeltmek",
|
"fix": "salak şeyleri düzeltmek",
|
||||||
"ctf": "CTF challenge'ları çözmek",
|
"ctf": "CTF challenge'ları çözmek",
|
||||||
@ -21,26 +23,62 @@
|
|||||||
"wiki": "wikimi genişletmek"
|
"wiki": "wikimi genişletmek"
|
||||||
},
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"title": "Eğer benim ile iletişime geçmek istiyorsanız, işte bazı faydalı linkler",
|
"title": "iletişim",
|
||||||
"prefer": "tercihim"
|
"desc": "Eğer benim ile iletişime geçmek istiyorsanız, işte bazı faydalı linkler",
|
||||||
|
"prefer": "Email'i fazlasıyla tercih ediyorum, PGP anahtarım ile şifreli email'ler gönderebilirsiniz"
|
||||||
},
|
},
|
||||||
"info": {
|
"services": {
|
||||||
"title": "Salak şeyler inşa etmenin yanı sıra, herkes için kullanıma açık özgür ve ücretsiz servisler host ediyorum",
|
"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",
|
"speed": "Tüm servisler 600 Mbit/s ağ arayüzü üzerinden erişilebilir",
|
||||||
"security": "Hepsi SSL şifreli bağlantı kullanıyor ve hepsi gizliğinize saygı gösteriyor",
|
"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": "Accessible from clearnet, TOR and I2P, no region or network blocks",
|
||||||
"privacy": "Açık ağdan, TOR ve I2P'den erişilebilirler, bölge ya da ağ blokları yok",
|
"privacy": "Açık ağdan, TOR ve I2P'den erişilebilirler, bölge ya da ağ blokları yok",
|
||||||
"bullshit": "CDN yok, cloudflare yok, CAPTCHA yok, analitikler yok, boktan saçmalıklar yok",
|
"bullshit": "CDN yok, cloudflare yok, CAPTCHA yok, analitikler yok, boktan saçmalıklar yok",
|
||||||
"link": "tüm servisleri incele"
|
"link": "Tüm servisleri incele!"
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"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": {
|
"footer": {
|
||||||
"source": "Kaynak",
|
"source": "Kaynak",
|
||||||
"license": "Lisans",
|
"license": "Lisans",
|
||||||
"privacy": "Gizlilik",
|
"privacy": "Gizlilik",
|
||||||
"powered": "Svelte, Go, SQLite ve yemek param tarafından destekleniyor",
|
"powered": "Svelte, Go, SQLite ve bağışlar tarafından destekleniyor",
|
||||||
"number": "{count}. ziyaretçisiniz",
|
"number": "{since} tarihinden beri {total} kez ziyaret edildi",
|
||||||
"congrat": "tebrikler!!",
|
"wow": "vay be!!",
|
||||||
"version": "Kullan API versiyonu {api_version}, arayüz versiyonu {frontend_version}"
|
"version": "Kullan API versiyonu {api_version}, arayüz versiyonu {frontend_version}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,6 @@
|
|||||||
import { locale, waitLocale } from "svelte-i18n";
|
import { locale_setup, locale_wait } from "$lib/locale.js";
|
||||||
import { init, register } from "svelte-i18n";
|
|
||||||
import { browser } from "$app/environment";
|
|
||||||
import languages from "$lib/lang.js";
|
|
||||||
|
|
||||||
const defaultLocale = languages[0].code;
|
export async function load() {
|
||||||
|
locale_setup();
|
||||||
for (let i = 0; i < languages.length; i++)
|
await locale_wait();
|
||||||
register(languages[i].code, () => import(/* @vite-ignore */ languages[i].path));
|
}
|
||||||
|
|
||||||
init({
|
|
||||||
fallbackLocale: defaultLocale,
|
|
||||||
initialLocale: browser ? window.navigator.language.slice(0, 2).toLowerCase() : defaultLocale,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const load = async () => {
|
|
||||||
if (browser) locale.set(window.navigator.language);
|
|
||||||
await waitLocale();
|
|
||||||
};
|
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
<script>
|
<script>
|
||||||
import Navbar from "$lib/navbar.svelte";
|
import Navbar from "$lib/navbar.svelte";
|
||||||
import Footer from "$lib/footer.svelte";
|
import Footer from "$lib/footer.svelte";
|
||||||
|
|
||||||
|
import { locale_select } from "$lib/locale.js";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
locale_select();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot></slot>
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@import "../../static/global.css";
|
@import "/global.css";
|
||||||
|
|
||||||
main {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
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,18 +1,23 @@
|
|||||||
<script>
|
<script>
|
||||||
import Header from "$lib/header.svelte";
|
import Header from "$lib/header.svelte";
|
||||||
import Content from "$lib/content.svelte";
|
import Error from "$lib/error.svelte";
|
||||||
import Head from "$lib/head.svelte";
|
import Head from "$lib/head.svelte";
|
||||||
import Card from "$lib/card.svelte";
|
import Card from "$lib/card.svelte";
|
||||||
import Link from "$lib/link.svelte";
|
import Link from "$lib/link.svelte";
|
||||||
|
|
||||||
|
import { _, locale } from "svelte-i18n";
|
||||||
import { color } from "$lib/util.js";
|
import { color } from "$lib/util.js";
|
||||||
import { _ } from "svelte-i18n";
|
|
||||||
|
let { data } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Head title="home" desc="home page of my personal website" />
|
<Head title="home" desc="home page of my personal website" />
|
||||||
<Header title={$_("home.title")} picture="tired" />
|
<Header picture="tired" title={$_("home.title")} />
|
||||||
|
|
||||||
<Content>
|
{#if data.error.length !== 0}
|
||||||
|
<Error error={data.error} />
|
||||||
|
{:else}
|
||||||
|
<main>
|
||||||
<Card title={$_("home.welcome.title")}>
|
<Card title={$_("home.welcome.title")}>
|
||||||
<span> 👋 {$_("home.welcome.desc")}</span>
|
<span> 👋 {$_("home.welcome.desc")}</span>
|
||||||
<ul>
|
<ul>
|
||||||
@ -25,6 +30,7 @@
|
|||||||
<span>{$_("home.work.desc")}</span>
|
<span>{$_("home.work.desc")}</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li>⌨️ {$_("home.work.build")}</li>
|
<li>⌨️ {$_("home.work.build")}</li>
|
||||||
|
<li>🤦 {$_("home.work.fix")}</li>
|
||||||
<li>🚩 {$_("home.work.ctf")}</li>
|
<li>🚩 {$_("home.work.ctf")}</li>
|
||||||
<li>👥 {$_("home.work.contribute")}</li>
|
<li>👥 {$_("home.work.contribute")}</li>
|
||||||
<li>📑 {$_("home.work.wiki")}</li>
|
<li>📑 {$_("home.work.wiki")}</li>
|
||||||
@ -34,68 +40,82 @@
|
|||||||
<span>{$_("home.links.desc")}:</span>
|
<span>{$_("home.links.desc")}:</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<Link icon="nf-fa-key" link="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D">
|
<Link
|
||||||
|
icon="nf-fa-key"
|
||||||
|
link="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D"
|
||||||
|
>
|
||||||
PGP
|
PGP
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link icon="nf-md-email" link="mailto:ngn@ngn.tf">Email</Link>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link icon="nf-md-mastodon" link="https://defcon.social/@ngn">Mastodon</Link>
|
<Link icon="nf-md-mastodon" link="https://defcon.social/@ngn">Mastodon</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<Link icon="nf-cod-github" link="https://github.com/ngn13">Github</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link icon="nf-md-email" link="mailto:ngn@ngn.tf">Email</Link>
|
|
||||||
<span class="prefer">({$_("home.links.prefer")})</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
|
||||||
<Card title={$_("home.info.title")}>
|
|
||||||
<div class="services">
|
|
||||||
<div class="info">
|
|
||||||
<span>
|
<span>
|
||||||
{$_("home.info.desc")}
|
{$_("home.links.prefer")}
|
||||||
|
</span>
|
||||||
|
</Card>
|
||||||
|
<Card title={$_("home.services.title")}>
|
||||||
|
<span>
|
||||||
|
{$_("home.services.desc")}:
|
||||||
</span>
|
</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<i style="color: var(--{color()});" class="nf nf-md-speedometer_slow"></i>
|
<i style="color: var(--{color()});" class="nf nf-md-speedometer_slow"></i>
|
||||||
{$_("home.info.speed")}
|
{$_("home.services.speed")}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i style="color: var(--{color()});" class="nf nf-fa-lock"></i>
|
<i style="color: var(--{color()});" class="nf nf-fa-lock"></i>
|
||||||
{$_("home.info.security")}
|
{$_("home.services.security")}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i style="color: var(--{color()});" class="nf nf-fa-network_wired"></i>
|
<i style="color: var(--{color()});" class="nf nf-fa-network_wired"></i>
|
||||||
{$_("home.info.privacy")}
|
{$_("home.services.privacy")}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i style="color: var(--{color()});" class="nf nf-md-eye_off"></i>
|
<i style="color: var(--{color()});" class="nf nf-md-eye_off"></i>
|
||||||
{$_("home.info.bullshit")}
|
{$_("home.services.bullshit")}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
<Link link="/services">{$_("home.services.link")}</Link>
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
</Content>
|
<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>
|
||||||
.prefer {
|
main {
|
||||||
color: var(--white-2);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
padding: 50px;
|
||||||
gap: 28px;
|
gap: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.services .info {
|
@media only screen and (max-width: 900px) {
|
||||||
display: flex;
|
main {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</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,39 +1,39 @@
|
|||||||
<script>
|
<script>
|
||||||
import Header from "$lib/header.svelte";
|
import Header from "$lib/header.svelte";
|
||||||
import Head from "$lib/head.svelte";
|
import Head from "$lib/head.svelte";
|
||||||
|
import Icon from "$lib/icon.svelte";
|
||||||
|
|
||||||
import { color } from "$lib/util.js";
|
import { color } from "$lib/util.js";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Head title="donate" desc="give me all of your life savings" />
|
<Head title="donate" desc="give me all of your life savings" />
|
||||||
<Header title="donate money!" picture="money" />
|
<Header picture="money" title={$_("donate.title")} />
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<span
|
<span> </span>
|
||||||
>I spend a lot of time working on different projects and maintaining different services.</span
|
<span>
|
||||||
>
|
{$_("donate.info")}
|
||||||
<span
|
{$_("donate.price")}
|
||||||
>I also spend a lot of money, but unlike time, you don't usually get much of it for free.</span
|
</span>
|
||||||
>
|
|
||||||
<span
|
|
||||||
>I mostly pay for hosting and electricity. Which when added up costs around 550₺ per month, that
|
|
||||||
is Turkish Lira, equals to ~$15 at time of writing.</span
|
|
||||||
>
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<span
|
<span>
|
||||||
>So even a small donation would be highly appreciated and it would help me keep everything up
|
{$_("donate.details")}
|
||||||
and running.</span
|
</span>
|
||||||
>
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="color: var(--{color()})">Platform</th>
|
<th style="color: var(--{color()})">{$_("donate.table.platform")}</th>
|
||||||
<th style="color: var(--{color()})">Address/Link</th>
|
<th style="color: var(--{color()})">{$_("donate.table.address")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Monero (XMR)</td>
|
<td>
|
||||||
|
<Icon icon="nf-fa-monero" />
|
||||||
|
Monero (XMR)
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>
|
<code>
|
||||||
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
|
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
|
||||||
@ -42,10 +42,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<span
|
<span>
|
||||||
>Also huge thanks to all of you who has donated so far, as I said, I highly appreciate it. Thank
|
{$_("donate.thanks")}
|
||||||
you!</span
|
</span>
|
||||||
>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -77,6 +76,7 @@
|
|||||||
|
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
|
font-size: var(--size-4);
|
||||||
border: solid 1px var(--black-4);
|
border: solid 1px var(--black-4);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import { services } from "$lib/api.js";
|
import { api_get_services } from "$lib/api.js";
|
||||||
|
|
||||||
export async function load({ fetch }) {
|
export async function load({ fetch }) {
|
||||||
|
try {
|
||||||
|
let services = await api_get_services(fetch);
|
||||||
return {
|
return {
|
||||||
list: await services(fetch),
|
services: null === services ? [] : services,
|
||||||
|
error: "",
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
error: err.toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -1,49 +1,62 @@
|
|||||||
<script>
|
<script>
|
||||||
import Service from "$lib/service.svelte";
|
import Service from "$lib/service.svelte";
|
||||||
import Header from "$lib/header.svelte";
|
import Header from "$lib/header.svelte";
|
||||||
|
import Error from "$lib/error.svelte";
|
||||||
import Link from "$lib/link.svelte";
|
import Link from "$lib/link.svelte";
|
||||||
import Head from "$lib/head.svelte";
|
import Head from "$lib/head.svelte";
|
||||||
|
|
||||||
import { api_url } from "$lib/util.js";
|
import { api_urljoin } from "$lib/api.js";
|
||||||
import { locale } from "svelte-i18n";
|
import { locale, _ } from "svelte-i18n";
|
||||||
|
|
||||||
export let data;
|
let { data } = $props();
|
||||||
|
let services = $state(data.services);
|
||||||
let list = data.list,
|
|
||||||
services = list;
|
|
||||||
let value = "";
|
|
||||||
|
|
||||||
function change(input) {
|
function change(input) {
|
||||||
value = input.target.value.toLowerCase();
|
let value = input.target.value.toLowerCase();
|
||||||
services = [];
|
services = [];
|
||||||
|
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
services = list;
|
services = data.services;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.forEach((s) => {
|
data.services.forEach((s) => {
|
||||||
if (s.name.toLowerCase().includes(value)) services.push(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>
|
||||||
|
|
||||||
<Head title="services" desc="my self-hosted services and projects" />
|
<Head title="services" desc="my self-hosted services and projects" />
|
||||||
<Header title="service status" picture="cool" />
|
<Header picture="cool" title={$_("services.title")} />
|
||||||
|
|
||||||
|
{#if data.error.length !== 0}
|
||||||
|
<Error error={data.error} />
|
||||||
|
{:else}
|
||||||
<main>
|
<main>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<input on:input={change} type="text" placeholder="Search for a service" />
|
<input oninput={change} type="text" placeholder={$_("services.search")} />
|
||||||
<div>
|
<div>
|
||||||
<Link icon="nf-fa-feed" link={api_url("/news/" + $locale.slice(0, 2))}>News and updates</Link>
|
<Link icon="nf-fa-feed" link={api_urljoin("/news/" + $locale)}>{$_("services.feed")}</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="services">
|
<div class="services">
|
||||||
{#each services as service}
|
{#if get_services().length == 0}
|
||||||
|
<h3 class="none">{$_("services.none")}</h3>
|
||||||
|
{:else}
|
||||||
|
{#each get_services() as service}
|
||||||
<Service {service} />
|
<Service {service} />
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
@ -58,12 +71,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main .none {
|
||||||
|
color: var(--white-3);
|
||||||
|
}
|
||||||
|
|
||||||
main .services {
|
main .services {
|
||||||
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin-top: 20px;
|
|
||||||
gap: 28px;
|
gap: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,82 +4,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes colorAnimation {
|
@keyframes cursor {
|
||||||
100%,
|
to {
|
||||||
0% {
|
border-color: transparent;
|
||||||
color: rgb(255, 0, 0);
|
|
||||||
}
|
|
||||||
8% {
|
|
||||||
color: rgb(255, 127, 0);
|
|
||||||
}
|
|
||||||
16% {
|
|
||||||
color: rgb(255, 255, 0);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
color: rgb(127, 255, 0);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
color: rgb(0, 255, 0);
|
|
||||||
}
|
|
||||||
41% {
|
|
||||||
color: rgb(0, 255, 127);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
color: rgb(0, 255, 255);
|
|
||||||
}
|
|
||||||
58% {
|
|
||||||
color: rgb(0, 127, 255);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
color: rgb(0, 0, 255);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
color: rgb(127, 0, 255);
|
|
||||||
}
|
|
||||||
83% {
|
|
||||||
color: rgb(255, 0, 255);
|
|
||||||
}
|
|
||||||
91% {
|
|
||||||
color: rgb(255, 0, 127);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes borderAnimation {
|
@keyframes typing {
|
||||||
100%,
|
from {
|
||||||
0% {
|
width: 0%;
|
||||||
border-bottom-color: rgb(255, 0, 0);
|
|
||||||
}
|
}
|
||||||
8% {
|
|
||||||
border-bottom-color: rgb(255, 127, 0);
|
to {
|
||||||
}
|
width: 100%;
|
||||||
16% {
|
|
||||||
border-bottom-color: rgb(255, 255, 0);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
border-bottom-color: rgb(127, 255, 0);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
border-bottom-color: rgb(0, 255, 0);
|
|
||||||
}
|
|
||||||
41% {
|
|
||||||
border-bottom-color: rgb(0, 255, 127);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
border-bottom-color: rgb(0, 255, 255);
|
|
||||||
}
|
|
||||||
58% {
|
|
||||||
border-bottom-color: rgb(0, 127, 255);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
border-bottom-color: rgb(0, 0, 255);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
border-bottom-color: rgb(127, 0, 255);
|
|
||||||
}
|
|
||||||
83% {
|
|
||||||
border-bottom-color: rgb(255, 0, 255);
|
|
||||||
}
|
|
||||||
91% {
|
|
||||||
border-bottom-color: rgb(255, 0, 127);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
app/static/banner.png
Normal file
BIN
app/static/banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
@ -25,10 +25,14 @@
|
|||||||
--size-4: 20px;
|
--size-4: 20px;
|
||||||
--size-5: 24px;
|
--size-5: 24px;
|
||||||
--size-6: 30px;
|
--size-6: 30px;
|
||||||
--size-7: 50px;
|
|
||||||
|
|
||||||
--text-shadow: 0px 5px 20px rgba(90, 90, 90, 0.8);
|
--box-shadow-1: rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px;
|
||||||
--box-shadow: 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@ -36,6 +40,10 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--black-1);
|
background: var(--black-1);
|
||||||
font-family: "Ubuntu", sans-serif;
|
font-family: "Ubuntu", sans-serif;
|
||||||
|
@ -61,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -725,7 +720,7 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -770,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,
|
||||||
|
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 |
@ -3,12 +3,33 @@ import { defineConfig } from "vite";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { readFileSync } from "fs";
|
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 file = fileURLToPath(new URL("package.json", import.meta.url));
|
||||||
const json = readFileSync(file, "utf8");
|
const json = readFileSync(file, "utf8");
|
||||||
const pkg = JSON.parse(json);
|
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: {
|
define: {
|
||||||
pkg: pkg,
|
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
|
17
doc/Dockerfile
Normal file
17
doc/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
FROM ghcr.io/ngn13/ctorm:1.7
|
||||||
|
|
||||||
|
WORKDIR /doc
|
||||||
|
|
||||||
|
COPY Makefile ./
|
||||||
|
COPY docs ./docs
|
||||||
|
COPY inc ./inc
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
RUN useradd runner -r -u 1001 -d /doc
|
||||||
|
RUN chown -R runner:runner /doc
|
||||||
|
|
||||||
|
USER runner
|
||||||
|
RUN make
|
||||||
|
|
||||||
|
EXPOSE 7003
|
||||||
|
ENTRYPOINT ["/doc/doc.elf"]
|
35
doc/Makefile
Normal file
35
doc/Makefile
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# dirs
|
||||||
|
DIRS = $(shell find src/* -type d)
|
||||||
|
DISTDIR = dist
|
||||||
|
OUTDIRS = $(patsubst src/%,$(DISTDIR)/%,$(DIRS))
|
||||||
|
|
||||||
|
# sources
|
||||||
|
HSRCS = $(wildcard inc/*.h)
|
||||||
|
CSRCS = $(shell find -type f -name '*.c')
|
||||||
|
OBJS = $(patsubst ./src/%.c,./$(DISTDIR)/%.o,$(CSRCS))
|
||||||
|
|
||||||
|
# compiler flags
|
||||||
|
CFLAGS = -O3 -fstack-protector-strong -fcf-protection=full -fstack-clash-protection
|
||||||
|
LIBS = -lctorm -lcjson
|
||||||
|
INCLUDE = -I./inc
|
||||||
|
|
||||||
|
all: doc.elf
|
||||||
|
|
||||||
|
doc.elf: $(OBJS)
|
||||||
|
echo $(OBJS) $(OUTDIRS)
|
||||||
|
gcc $(CFLAGS) -o $@ $^ $(LIBS)
|
||||||
|
|
||||||
|
$(DISTDIR)/%.o: src/%.c
|
||||||
|
@mkdir -pv $(OUTDIRS)
|
||||||
|
gcc $(CFLAGS) $(INCLUDE) -c -o $@ $^ $(LIBS)
|
||||||
|
|
||||||
|
format:
|
||||||
|
clang-format -i -style=file $(CSRCS) $(HSRCS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(DISTDIR)
|
||||||
|
|
||||||
|
run:
|
||||||
|
./doc.elf
|
||||||
|
|
||||||
|
.PHONY: format clean run
|
4
doc/docs/api.en.json
Normal file
4
doc/docs/api.en.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"title": "API documentation",
|
||||||
|
"desc": "Website's API documentation"
|
||||||
|
}
|
@ -1,40 +1,26 @@
|
|||||||
<!-- This is the markdown file that will be served by the index route -->
|
My website's API stores information about my self-hosted services, it also allows me to
|
||||||
|
publish news and updates about these services using an Atom feed and it keeps track of
|
||||||
|
visitor metrics. The API itself is written in Go and uses SQLite for storage.
|
||||||
|
|
||||||
<style>
|
This documentation contains information about all the available API endpoints.
|
||||||
* {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
# [{{.api.Host}}]({{.api.String}})
|
|
||||||
This is the API for my personal website, [{{.frontend.Host}}]({{.frontend.String}}).
|
|
||||||
|
|
||||||
It stores information about the self-hosted services I provide and it also allows me
|
|
||||||
to publish news and updates about these services using an Atom feed. It's written in
|
|
||||||
Go and uses SQLite for storage. Licensed under GNU GPL version 3.
|
|
||||||
|
|
||||||
**Source code and the license is available at**: [https://github.com/ngn13/website](https://github.com/ngn13/website)
|
|
||||||
**You can report issues to**: [https://github.com/ngn13/website/issues](https://github.com/ngn13/website/issues)
|
|
||||||
|
|
||||||
The rest of this document contains documentation for all the available API endpoints.
|
|
||||||
|
|
||||||
## Version 1 Endpoints
|
## Version 1 Endpoints
|
||||||
Each version 1 endpoint, can be accessed using the /v1 route.
|
Each version 1 endpoint, can be accessed using the `/v1` route.
|
||||||
|
|
||||||
All the endpoints return JSON formatted data.
|
All the endpoints return JSON formatted data.
|
||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
If any error occurs, you will get a non-200 response. And the JSON data will have an
|
If any error occurs, you will get a non-200 response. And the JSON data will have an
|
||||||
"error" key, which will contain information about the error that occured, in the
|
`error` key, which will contain information about the error that occured, in the
|
||||||
string format. This is the only JSON key that will be set in non-200 responses.
|
string format. This is the only JSON key that will be set in non-200 responses.
|
||||||
|
|
||||||
### Results
|
### Results
|
||||||
If no error occurs, "error" key will be set to an emtpy string (""). If the endpoint
|
If no error occurs, `error` key will be set to an emtpy string (""). If the endpoint
|
||||||
returns any data, this will be stored using the "result" key. The "result" have a
|
returns any data, this will be stored using the `result` key. The `result` have a
|
||||||
different expected type and a format for each endpoint.
|
different expected type and a format for each endpoint.
|
||||||
|
|
||||||
### Multilang
|
### Multilang
|
||||||
Some "result" formats may use a structure called "Multilang". This is a simple JSON
|
Some `result` formats may use a structure called "Multilang". This is a simple JSON
|
||||||
structure that includes one key for each supported language. The key is named after
|
structure that includes one key for each supported language. The key is named after
|
||||||
the language it represents. Currently only supported languages are:
|
the language it represents. Currently only supported languages are:
|
||||||
- English (`en`)
|
- English (`en`)
|
||||||
@ -49,14 +35,14 @@ Here is an example multilang structure:
|
|||||||
"tr": "Merhaba, dünya!"
|
"tr": "Merhaba, dünya!"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
If a "result" field is using a multilang structure, it will be specified as "Multilang"
|
If a `result` field is using a multilang structure, it will be specified as "Multilang"
|
||||||
in the rest of the documentation.
|
in the rest of the documentation.
|
||||||
|
|
||||||
### Administrator routes
|
### Administrator routes
|
||||||
The endpoints under the "/v1/admin" route, are administrator-only routes. To access
|
The endpoints under the `/v1/admin` route, are administrator-only routes. To access
|
||||||
these routes you'll need to specfiy and password using the "Authorization" header.
|
these routes you'll need to specfiy a password using the `Authorization` header. If
|
||||||
If the password you specify, matches with the password specified using the
|
the password you specify, matches with the password specified using the `API_PASSWORD`
|
||||||
`API_PASSWORD` environment variable, you will be able to access the route.
|
environment variable, you will be able to access the route.
|
||||||
|
|
||||||
### GET /v1/services
|
### GET /v1/services
|
||||||
Returns a list of available services. Each service has the following JSON format:
|
Returns a list of available services. Each service has the following JSON format:
|
||||||
@ -91,12 +77,24 @@ not supported for this service/status checking is disabled (integer, UNIX timest
|
|||||||
- `i2p`: I2P URL for the service (string, empty string if none)
|
- `i2p`: I2P URL for the service (string, empty string if none)
|
||||||
|
|
||||||
You can also get information about a specific service by specifying it's name using
|
You can also get information about a specific service by specifying it's name using
|
||||||
a URL query named "name".
|
a URL query named `name`.
|
||||||
|
|
||||||
### GET /v1/news/:language
|
### GET /v1/news/:language
|
||||||
Returns a Atom feed of news for the given language. Supports languages that are supported
|
Returns a Atom feed of news for the given language. Supports languages that are supported
|
||||||
by Multilang.
|
by Multilang.
|
||||||
|
|
||||||
|
### GET /v1/metrics
|
||||||
|
Returns metrics about the API usage. The metric data has the following format:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"since":1736294400,
|
||||||
|
"total":8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Where:
|
||||||
|
- `since`: Metric collection start date (integer, UNIX timestamp)
|
||||||
|
- `total`: Total number of visitors (integer)
|
||||||
|
|
||||||
### GET /v1/admin/logs
|
### GET /v1/admin/logs
|
||||||
Returns a list of administrator logs. Each log has the following JSON format:
|
Returns a list of administrator logs. Each log has the following JSON format:
|
||||||
```
|
```
|
||||||
@ -109,18 +107,16 @@ Where:
|
|||||||
- `action`: Action that the administrator performed (string)
|
- `action`: Action that the administrator performed (string)
|
||||||
- `time`: Time when the administrator action was performed (integer, UNIX timestamp)
|
- `time`: Time when the administrator action was performed (integer, UNIX timestamp)
|
||||||
|
|
||||||
Client can get the logs for only a single address, by setting the URL query "addr".
|
|
||||||
|
|
||||||
### PUT /v1/admin/service/add
|
### PUT /v1/admin/service/add
|
||||||
Creates a new service. The request body needs to contain JSON data, and it needs to
|
Creates a new service. The request body needs to contain JSON data, and it needs to
|
||||||
have the JSON format used to represent a service. See "/v1/services/all" route to
|
have the JSON format used to represent a service. See `/v1/services/all` route to
|
||||||
see this format.
|
see this format.
|
||||||
|
|
||||||
Returns no data on success.
|
Returns no data on success.
|
||||||
|
|
||||||
### DELETE /v1/admin/service/del
|
### DELETE /v1/admin/service/del
|
||||||
Deletes a service. The client needs to specify the name of the service to delete, by
|
Deletes a service. The client needs to specify the name of the service to delete, by
|
||||||
setting the URL query "name".
|
setting the URL query `name`.
|
||||||
|
|
||||||
Returns no data on success.
|
Returns no data on success.
|
||||||
|
|
||||||
@ -156,6 +152,6 @@ Returns no data on success.
|
|||||||
|
|
||||||
### DELETE /v1/admin/news/del
|
### DELETE /v1/admin/news/del
|
||||||
Deletes a news post. The client needs to specify the ID of the news post to delete,
|
Deletes a news post. The client needs to specify the ID of the news post to delete,
|
||||||
by setting the URL query "id".
|
by setting the URL query `id`.
|
||||||
|
|
||||||
Returns no data on success.
|
Returns no data on success.
|
4
doc/docs/api.tr.json
Normal file
4
doc/docs/api.tr.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"title": "API dökümantasyonu",
|
||||||
|
"desc": "Websitesinin API dökümantasyonu"
|
||||||
|
}
|
159
doc/docs/api.tr.md
Normal file
159
doc/docs/api.tr.md
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
Websitemin API self-host edilen servisler hakkında bilgileri tutuyor, bu servisler hakkında
|
||||||
|
haberleri ve güncellemeleri bir Atom feed'i aracılığı ile paylaşmama izin veriyor ve ziyartçi
|
||||||
|
metriklerini takip ediyor. API'ın kendisi Go ile yazıldı ve veritabanı olarak SQLite kullanıyor.
|
||||||
|
|
||||||
|
Bu dökümentasyon tüm erişeme açık API endpoint'leri hakkında bilgiler içeriyor.
|
||||||
|
|
||||||
|
## Versyion 1 Endpoint'leri
|
||||||
|
Tüm versiyon 1 endpoint'leri `/v1` yolu ile erişilebilir.
|
||||||
|
|
||||||
|
Tüm endpoint'ler JSON ile formatlanmış veri döndürür.
|
||||||
|
|
||||||
|
### Hatalar
|
||||||
|
Herhangi bir hata ortaya çıkarsa 200 dışı bir cevap alırsınız. Ve JSON verisinde
|
||||||
|
bir `error` girdisi bulunur, bu ortaya çıkan hata hakkında gerekli bilgileri, metin formunda
|
||||||
|
içerir. 200 dışı bir cevap aldığınızda tek JSON girdisi bu olacaktır.
|
||||||
|
|
||||||
|
### Sonuçlar
|
||||||
|
Eğer bir hata ortaya çıkmaz ise, `error` girdisi boş bir metin olarak ayarlanır ("").
|
||||||
|
Eğer endpoint herhangi bir veri döndürüyorsa, bu veri `result` giridisi ile sağlanır.
|
||||||
|
Her endpoint için `result` girdisinin tipi farklı olabilir.
|
||||||
|
|
||||||
|
### Multilang
|
||||||
|
Bazı `result` formatları "Multilang" isimli bir yapıyı kullanabilir. Bu her desteklenen
|
||||||
|
dil için bir girdi bulunduran basit bir JSON yapısıdır. Her girdi ifade ettiği dil
|
||||||
|
ile isimlendirilir. Şuan tek desteklenen diller:
|
||||||
|
- English (`en`)
|
||||||
|
- Turkish (`tr`)
|
||||||
|
|
||||||
|
Yani her multilang yapısında, bu girdilerden **en az** bir tanesi bulunur.
|
||||||
|
|
||||||
|
İşte örnek bir multilang yapısı:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"en": "Hello, world!",
|
||||||
|
"tr": "Merhaba, dünya!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Bu dökümantasyonun geri kalanında, eğer bir `result` girdisi bir multilang yapısı kullanıyorsa,
|
||||||
|
"Multilang" olarak isimlendirilecek.
|
||||||
|
|
||||||
|
### Yönetici yolları
|
||||||
|
`/v1/admin` yolu altındaki endpoint'ler yöneticiye-özeldir. Bu yollara erişmek için,
|
||||||
|
`Authorization` header'ı aracılığı ile bir parola belirtmeniz gerekecektir. Eğer
|
||||||
|
belritiğiniz parola `API_PASSWORD` ortam değişkeni ile belirtilen parola ile
|
||||||
|
uyuşuyorsa, yola erişebilirsiniz.
|
||||||
|
|
||||||
|
### GET /v1/services
|
||||||
|
Erişilebilir servislerin bir listesini döndürür. Her servis şu JSON formatını
|
||||||
|
takip eder:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"name": "Test Service",
|
||||||
|
"desc": {
|
||||||
|
"en": "Service used for testing the API",
|
||||||
|
"tr": "API'ı test etmek için kullanılan servis"
|
||||||
|
},
|
||||||
|
"check_time": 1735861944,
|
||||||
|
"check_res": 1,
|
||||||
|
"check_url": "http://localhost:7001",
|
||||||
|
"clear": "http://localhost:7001",
|
||||||
|
"onion": "",
|
||||||
|
"i2p": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Burada:
|
||||||
|
- `name`: Servis ismi (metin)
|
||||||
|
- `desc`: Servis açıklaması (Multilang)
|
||||||
|
- `check_time`: Servisin en son durumunun kontrol edildiği zaman, eğer bu servis için
|
||||||
|
durum kontrolü desteklenmiyorsa/durum kontrolü devra dışı bırakılmış ise 0 olarak
|
||||||
|
ayarlanır (sayı, UNIX zaman damgası)
|
||||||
|
- `check_res`: En son servis durum kontrolünün sonucu (sayı)
|
||||||
|
* servis kapalı ise 0
|
||||||
|
* servis çalışıyor ise 1
|
||||||
|
* serivs çalışıyor, ama yavaş ise 2
|
||||||
|
* bu servis için durum kontrolü desteklenmiyorsa/durum kontrolü devre dışı ise 3
|
||||||
|
- `check_url`: Servis durum kontrolü için kullanılan URL (metin, yoksa boş metin)
|
||||||
|
- `clear`: Servisin açık ağ URL'si (metin, yoksa boş metin)
|
||||||
|
- `onion`: Servisin Onion (TOR) URL'si (metin, yoksa boş metin)
|
||||||
|
- `i2p`: Servisin I2P URL'si (metin, yoksa boş metin)
|
||||||
|
|
||||||
|
`name` isimli bir URL sorgusu ile servisin ismini belirterek, spesifik bir servis hakkında
|
||||||
|
bilgi de alabilirsiniz.
|
||||||
|
|
||||||
|
### GET /v1/news/:language
|
||||||
|
Verilen dil için haberlerin bir Atom feed'i döndürür. Multilang tarafından desteklenen
|
||||||
|
dilleri destekler.
|
||||||
|
|
||||||
|
### GET /v1/metrics
|
||||||
|
API kullanımı hakkınadaki metrikleri döndürür. Metrik şu formatı kullanır:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"since":1736294400,
|
||||||
|
"total":8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Burada:
|
||||||
|
- `since`: Metrik toplama başlangıç tarihi (sayı, UNIX zaman damgası)
|
||||||
|
- `total`: Toplam ziyaretçi sayısı (sayı)
|
||||||
|
|
||||||
|
### GET /v1/admin/logs
|
||||||
|
Yönetici kayıtlarının bir listesini döndürür. Her kayıt şu JSON formatını takip eder:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"action": "Added service \"Test Service\"",
|
||||||
|
"time": 1735861794
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Burada:
|
||||||
|
- `action`: Yöneticinin yaptığı eylem (metin)
|
||||||
|
- `time`: Yönetici eylemin yapıldığı zaman (sayı, UNIX zaman damgası)
|
||||||
|
|
||||||
|
### PUT /v1/admin/service/add
|
||||||
|
Yeni bir servis oluşturur. İstek gövdesinin servis için kullanılan JSON formatını
|
||||||
|
takip eden JSON verisini içermesi gerekir. Bu formatı görmek için `/v1/services/all`
|
||||||
|
yoluna bakınız.
|
||||||
|
|
||||||
|
Başarılı ise herhangi bir veri döndürmez.
|
||||||
|
|
||||||
|
### DELETE /v1/admin/service/del
|
||||||
|
Bir servisi siler. İstemcinin `name` URL sorgusu ile silinecek servisin ismini belirtmesi
|
||||||
|
gerekir.
|
||||||
|
|
||||||
|
Başarılı ise herhangi bir veri döndürmez.
|
||||||
|
|
||||||
|
### GET /v1/admin/service/check
|
||||||
|
Tüm servisler için bir durum kontrolünü zorlar.
|
||||||
|
|
||||||
|
Başarılı ise herhangi bir veri döndürmez.
|
||||||
|
|
||||||
|
### PUT /v1/admin/news/add
|
||||||
|
Yeni bir haber paylaşımı oluşturur. İstek gövedisinin JSOn verisi içermesi ve verilen formatı
|
||||||
|
takip etmesi gerekir:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": "test_news",
|
||||||
|
"title": {
|
||||||
|
"en": "Very important news",
|
||||||
|
"tr": "Çok önemli haber"
|
||||||
|
},
|
||||||
|
"author": "ngn",
|
||||||
|
"content": {
|
||||||
|
"en": "Just letting you know that I'm testing the API",
|
||||||
|
"tr": "Sadece API'ı test ettiğimi bilmenizi istedim"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Burada:
|
||||||
|
- `id`: Haber paylaşımının benzersiz ID'si (metin)
|
||||||
|
- `title`: Haber paylaşımının başlığı (Multilang)
|
||||||
|
- `author`: Haber paylaşımının yazarı (metin)
|
||||||
|
- `content`: Haber paylaşımının içerği (Multilang)
|
||||||
|
|
||||||
|
Başarılı ise herhangi bir veri döndürmez.
|
||||||
|
|
||||||
|
### DELETE /v1/admin/news/del
|
||||||
|
Bir haber paylaşımı siler. İstemcinin `id` URL sorgusu ile silinecek paylaşımın ID'sini
|
||||||
|
belirtmesi gerekir.
|
||||||
|
|
||||||
|
Başarılı ise herhangi bir veri döndürmez.
|
4
doc/docs/license.en.json
Normal file
4
doc/docs/license.en.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"title": "License",
|
||||||
|
"desc": "Source code license"
|
||||||
|
}
|
660
doc/docs/license.en.md
Normal file
660
doc/docs/license.en.md
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||||
|
<https://fsf.org/>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this
|
||||||
|
license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
## Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains
|
||||||
|
free software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing
|
||||||
|
under this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
## TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
### 0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds
|
||||||
|
of works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of
|
||||||
|
an exact copy. The resulting work is called a "modified version" of
|
||||||
|
the earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user
|
||||||
|
through a computer network, with no transfer of a copy, is not
|
||||||
|
conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices" to
|
||||||
|
the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
### 1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. "Object code" means any non-source form of
|
||||||
|
a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users can
|
||||||
|
regenerate automatically from other parts of the Corresponding Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that same
|
||||||
|
work.
|
||||||
|
|
||||||
|
### 2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not convey,
|
||||||
|
without conditions so long as your license otherwise remains in force.
|
||||||
|
You may convey covered works to others for the sole purpose of having
|
||||||
|
them make modifications exclusively for you, or provide you with
|
||||||
|
facilities for running those works, provided that you comply with the
|
||||||
|
terms of this License in conveying all material for which you do not
|
||||||
|
control copyright. Those thus making or running the covered works for
|
||||||
|
you must do so exclusively on your behalf, under your direction and
|
||||||
|
control, on terms that prohibit them from making any copies of your
|
||||||
|
copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under the
|
||||||
|
conditions stated below. Sublicensing is not allowed; section 10 makes
|
||||||
|
it unnecessary.
|
||||||
|
|
||||||
|
### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such
|
||||||
|
circumvention is effected by exercising rights under this License with
|
||||||
|
respect to the covered work, and you disclaim any intention to limit
|
||||||
|
operation or modification of the work as a means of enforcing, against
|
||||||
|
the work's users, your or third parties' legal rights to forbid
|
||||||
|
circumvention of technological measures.
|
||||||
|
|
||||||
|
### 4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
### 5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
- a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
- b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under
|
||||||
|
section 7. This requirement modifies the requirement in section 4
|
||||||
|
to "keep intact all notices".
|
||||||
|
- c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
- d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
### 6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms of
|
||||||
|
sections 4 and 5, provided that you also convey the machine-readable
|
||||||
|
Corresponding Source under the terms of this License, in one of these
|
||||||
|
ways:
|
||||||
|
|
||||||
|
- a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
- b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the Corresponding
|
||||||
|
Source from a network server at no charge.
|
||||||
|
- c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
- d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
- e) Convey the object code using peer-to-peer transmission,
|
||||||
|
provided you inform other peers where the object code and
|
||||||
|
Corresponding Source of the work are being offered to the general
|
||||||
|
public at no charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal,
|
||||||
|
family, or household purposes, or (2) anything designed or sold for
|
||||||
|
incorporation into a dwelling. In determining whether a product is a
|
||||||
|
consumer product, doubtful cases shall be resolved in favor of
|
||||||
|
coverage. For a particular product received by a particular user,
|
||||||
|
"normally used" refers to a typical or common use of that class of
|
||||||
|
product, regardless of the status of the particular user or of the way
|
||||||
|
in which the particular user actually uses, or expects or is expected
|
||||||
|
to use, the product. A product is a consumer product regardless of
|
||||||
|
whether the product has substantial commercial, industrial or
|
||||||
|
non-consumer uses, unless such uses represent the only significant
|
||||||
|
mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to
|
||||||
|
install and execute modified versions of a covered work in that User
|
||||||
|
Product from a modified version of its Corresponding Source. The
|
||||||
|
information must suffice to ensure that the continued functioning of
|
||||||
|
the modified object code is in no case prevented or interfered with
|
||||||
|
solely because modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or
|
||||||
|
updates for a work that has been modified or installed by the
|
||||||
|
recipient, or for the User Product in which it has been modified or
|
||||||
|
installed. Access to a network may be denied when the modification
|
||||||
|
itself materially and adversely affects the operation of the network
|
||||||
|
or violates the rules and protocols for communication across the
|
||||||
|
network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
### 7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders
|
||||||
|
of that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
- a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
- b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
- c) Prohibiting misrepresentation of the origin of that material,
|
||||||
|
or requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
- d) Limiting the use for publicity purposes of names of licensors
|
||||||
|
or authors of the material; or
|
||||||
|
- e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
- f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions
|
||||||
|
of it) with contractual assumptions of liability to the recipient,
|
||||||
|
for any liability that these contractual assumptions directly
|
||||||
|
impose on those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions; the
|
||||||
|
above requirements apply either way.
|
||||||
|
|
||||||
|
### 8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your license
|
||||||
|
from a particular copyright holder is reinstated (a) provisionally,
|
||||||
|
unless and until the copyright holder explicitly and finally
|
||||||
|
terminates your license, and (b) permanently, if the copyright holder
|
||||||
|
fails to notify you of the violation by some reasonable means prior to
|
||||||
|
60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
### 9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or run
|
||||||
|
a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
### 10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
### 11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims owned
|
||||||
|
or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within the
|
||||||
|
scope of its coverage, prohibits the exercise of, or is conditioned on
|
||||||
|
the non-exercise of one or more of the rights that are specifically
|
||||||
|
granted under this License. You may not convey a covered work if you
|
||||||
|
are a party to an arrangement with a third party that is in the
|
||||||
|
business of distributing software, under which you make payment to the
|
||||||
|
third party based on the extent of your activity of conveying the
|
||||||
|
work, and under which the third party grants, to any of the parties
|
||||||
|
who would receive the covered work from you, a discriminatory patent
|
||||||
|
license (a) in connection with copies of the covered work conveyed by
|
||||||
|
you (or copies made from those copies), or (b) primarily for and in
|
||||||
|
connection with specific products or compilations that contain the
|
||||||
|
covered work, unless you entered into that arrangement, or that patent
|
||||||
|
license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
### 12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under
|
||||||
|
this License and any other pertinent obligations, then as a
|
||||||
|
consequence you may not convey it at all. For example, if you agree to
|
||||||
|
terms that obligate you to collect a royalty for further conveying
|
||||||
|
from those to whom you convey the Program, the only way you could
|
||||||
|
satisfy both those terms and this License would be to refrain entirely
|
||||||
|
from conveying the Program.
|
||||||
|
|
||||||
|
### 13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your
|
||||||
|
version supports such interaction) an opportunity to receive the
|
||||||
|
Corresponding Source of your version by providing access to the
|
||||||
|
Corresponding Source from a network server at no charge, through some
|
||||||
|
standard or customary means of facilitating copying of software. This
|
||||||
|
Corresponding Source shall include the Corresponding Source for any
|
||||||
|
work covered by version 3 of the GNU General Public License that is
|
||||||
|
incorporated pursuant to the following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
### 14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Affero General Public License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever
|
||||||
|
published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future versions
|
||||||
|
of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
### 15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
|
||||||
|
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
|
||||||
|
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
|
||||||
|
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
|
||||||
|
CORRECTION.
|
||||||
|
|
||||||
|
### 16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
|
||||||
|
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
|
||||||
|
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
|
||||||
|
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
|
||||||
|
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
||||||
|
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
||||||
|
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
### 17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
## How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these
|
||||||
|
terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to
|
||||||
|
attach them to the start of each source file to most effectively state
|
||||||
|
the exclusion of warranty; and each file should have at least the
|
||||||
|
"copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper
|
||||||
|
mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for
|
||||||
|
the specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. For more information on this, and how to apply and follow
|
||||||
|
the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
4
doc/docs/license.tr.json
Normal file
4
doc/docs/license.tr.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"title": "Lisans",
|
||||||
|
"desc": "Kaynak kodu lisansı"
|
||||||
|
}
|
660
doc/docs/license.tr.md
Normal file
660
doc/docs/license.tr.md
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||||
|
<https://fsf.org/>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this
|
||||||
|
license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
## Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains
|
||||||
|
free software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing
|
||||||
|
under this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
## TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
### 0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds
|
||||||
|
of works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of
|
||||||
|
an exact copy. The resulting work is called a "modified version" of
|
||||||
|
the earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user
|
||||||
|
through a computer network, with no transfer of a copy, is not
|
||||||
|
conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices" to
|
||||||
|
the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
### 1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. "Object code" means any non-source form of
|
||||||
|
a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users can
|
||||||
|
regenerate automatically from other parts of the Corresponding Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that same
|
||||||
|
work.
|
||||||
|
|
||||||
|
### 2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not convey,
|
||||||
|
without conditions so long as your license otherwise remains in force.
|
||||||
|
You may convey covered works to others for the sole purpose of having
|
||||||
|
them make modifications exclusively for you, or provide you with
|
||||||
|
facilities for running those works, provided that you comply with the
|
||||||
|
terms of this License in conveying all material for which you do not
|
||||||
|
control copyright. Those thus making or running the covered works for
|
||||||
|
you must do so exclusively on your behalf, under your direction and
|
||||||
|
control, on terms that prohibit them from making any copies of your
|
||||||
|
copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under the
|
||||||
|
conditions stated below. Sublicensing is not allowed; section 10 makes
|
||||||
|
it unnecessary.
|
||||||
|
|
||||||
|
### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such
|
||||||
|
circumvention is effected by exercising rights under this License with
|
||||||
|
respect to the covered work, and you disclaim any intention to limit
|
||||||
|
operation or modification of the work as a means of enforcing, against
|
||||||
|
the work's users, your or third parties' legal rights to forbid
|
||||||
|
circumvention of technological measures.
|
||||||
|
|
||||||
|
### 4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
### 5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
- a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
- b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under
|
||||||
|
section 7. This requirement modifies the requirement in section 4
|
||||||
|
to "keep intact all notices".
|
||||||
|
- c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
- d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
### 6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms of
|
||||||
|
sections 4 and 5, provided that you also convey the machine-readable
|
||||||
|
Corresponding Source under the terms of this License, in one of these
|
||||||
|
ways:
|
||||||
|
|
||||||
|
- a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
- b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the Corresponding
|
||||||
|
Source from a network server at no charge.
|
||||||
|
- c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
- d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
- e) Convey the object code using peer-to-peer transmission,
|
||||||
|
provided you inform other peers where the object code and
|
||||||
|
Corresponding Source of the work are being offered to the general
|
||||||
|
public at no charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal,
|
||||||
|
family, or household purposes, or (2) anything designed or sold for
|
||||||
|
incorporation into a dwelling. In determining whether a product is a
|
||||||
|
consumer product, doubtful cases shall be resolved in favor of
|
||||||
|
coverage. For a particular product received by a particular user,
|
||||||
|
"normally used" refers to a typical or common use of that class of
|
||||||
|
product, regardless of the status of the particular user or of the way
|
||||||
|
in which the particular user actually uses, or expects or is expected
|
||||||
|
to use, the product. A product is a consumer product regardless of
|
||||||
|
whether the product has substantial commercial, industrial or
|
||||||
|
non-consumer uses, unless such uses represent the only significant
|
||||||
|
mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to
|
||||||
|
install and execute modified versions of a covered work in that User
|
||||||
|
Product from a modified version of its Corresponding Source. The
|
||||||
|
information must suffice to ensure that the continued functioning of
|
||||||
|
the modified object code is in no case prevented or interfered with
|
||||||
|
solely because modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or
|
||||||
|
updates for a work that has been modified or installed by the
|
||||||
|
recipient, or for the User Product in which it has been modified or
|
||||||
|
installed. Access to a network may be denied when the modification
|
||||||
|
itself materially and adversely affects the operation of the network
|
||||||
|
or violates the rules and protocols for communication across the
|
||||||
|
network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
### 7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders
|
||||||
|
of that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
- a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
- b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
- c) Prohibiting misrepresentation of the origin of that material,
|
||||||
|
or requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
- d) Limiting the use for publicity purposes of names of licensors
|
||||||
|
or authors of the material; or
|
||||||
|
- e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
- f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions
|
||||||
|
of it) with contractual assumptions of liability to the recipient,
|
||||||
|
for any liability that these contractual assumptions directly
|
||||||
|
impose on those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions; the
|
||||||
|
above requirements apply either way.
|
||||||
|
|
||||||
|
### 8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your license
|
||||||
|
from a particular copyright holder is reinstated (a) provisionally,
|
||||||
|
unless and until the copyright holder explicitly and finally
|
||||||
|
terminates your license, and (b) permanently, if the copyright holder
|
||||||
|
fails to notify you of the violation by some reasonable means prior to
|
||||||
|
60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
### 9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or run
|
||||||
|
a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
### 10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
### 11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims owned
|
||||||
|
or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within the
|
||||||
|
scope of its coverage, prohibits the exercise of, or is conditioned on
|
||||||
|
the non-exercise of one or more of the rights that are specifically
|
||||||
|
granted under this License. You may not convey a covered work if you
|
||||||
|
are a party to an arrangement with a third party that is in the
|
||||||
|
business of distributing software, under which you make payment to the
|
||||||
|
third party based on the extent of your activity of conveying the
|
||||||
|
work, and under which the third party grants, to any of the parties
|
||||||
|
who would receive the covered work from you, a discriminatory patent
|
||||||
|
license (a) in connection with copies of the covered work conveyed by
|
||||||
|
you (or copies made from those copies), or (b) primarily for and in
|
||||||
|
connection with specific products or compilations that contain the
|
||||||
|
covered work, unless you entered into that arrangement, or that patent
|
||||||
|
license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
### 12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under
|
||||||
|
this License and any other pertinent obligations, then as a
|
||||||
|
consequence you may not convey it at all. For example, if you agree to
|
||||||
|
terms that obligate you to collect a royalty for further conveying
|
||||||
|
from those to whom you convey the Program, the only way you could
|
||||||
|
satisfy both those terms and this License would be to refrain entirely
|
||||||
|
from conveying the Program.
|
||||||
|
|
||||||
|
### 13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your
|
||||||
|
version supports such interaction) an opportunity to receive the
|
||||||
|
Corresponding Source of your version by providing access to the
|
||||||
|
Corresponding Source from a network server at no charge, through some
|
||||||
|
standard or customary means of facilitating copying of software. This
|
||||||
|
Corresponding Source shall include the Corresponding Source for any
|
||||||
|
work covered by version 3 of the GNU General Public License that is
|
||||||
|
incorporated pursuant to the following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
### 14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Affero General Public License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever
|
||||||
|
published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future versions
|
||||||
|
of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
### 15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
|
||||||
|
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
|
||||||
|
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
|
||||||
|
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
|
||||||
|
CORRECTION.
|
||||||
|
|
||||||
|
### 16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
|
||||||
|
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
|
||||||
|
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
|
||||||
|
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
|
||||||
|
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
||||||
|
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
||||||
|
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
### 17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
## How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these
|
||||||
|
terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to
|
||||||
|
attach them to the start of each source file to most effectively state
|
||||||
|
the exclusion of warranty; and each file should have at least the
|
||||||
|
"copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper
|
||||||
|
mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for
|
||||||
|
the specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. For more information on this, and how to apply and follow
|
||||||
|
the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
4
doc/docs/privacy.en.json
Normal file
4
doc/docs/privacy.en.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"title": "Privacy",
|
||||||
|
"desc": "Learn how I respect your privacy"
|
||||||
|
}
|
41
doc/docs/privacy.en.md
Normal file
41
doc/docs/privacy.en.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
As a privacy advocate myself I do my best to respect your privacy while simultaneously keeping my server safe. I also think it's
|
||||||
|
important to be transparent about this kind of stuff so I wrote this document to tell you how exactly I process or store your
|
||||||
|
information.
|
||||||
|
|
||||||
|
## DNS & SSL
|
||||||
|
Currently I use cloudflare's name servers, however cloudflare doesn't own my domain (I didn't purchase the domain from cloudflare)
|
||||||
|
nor it proxies any of the traffic. All of my records use the *DNS only*" mode, meaning they are just DNS records and they point
|
||||||
|
to my server, not cloudflare. This also means cloudflare doesn't control any of the SSL certificates. All the certificates are
|
||||||
|
stored on my server and they are created with Let's Encrypt. So I own the certificates and it's not possible for cloudlfare to
|
||||||
|
suddenly switch DNS records (it would break SSL).
|
||||||
|
|
||||||
|
## Usage metrics
|
||||||
|
I don't have any kind of request or traffic monitoring on my server. So no, your HTTP(S) requests or other network
|
||||||
|
connections are not processed to generate colorful graphs, pie charts and stuff.
|
||||||
|
|
||||||
|
You may have realized that in the bottom of this page there is total visitor number tracker. It is the only
|
||||||
|
usage/visitor metric tracking I have, which is implemented by website's API, which is free (as in freedom) so you can
|
||||||
|
go audit it's code yourself.
|
||||||
|
|
||||||
|
I want to make it clear that this metric tracker does not store any information about you or your HTTP(S) requests to
|
||||||
|
a database. It temporarily stores your IP address' SHA1 hash, in memory, so it doesn't recount the same visitor again
|
||||||
|
when they refresh the website or visit it multiple times in a short period of time. After a certain amount of requests,
|
||||||
|
your IP address' SHA1 hash will be removed from the memory and it will be replaced with a new visitor's SHA1 hash instead.
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
All the HTTP(S) services are proxied with nginx, and nginx logs all of them to a file on the disk. This file (`access.log`)
|
||||||
|
is only readable by the root user, and it is contents are deleted every 4 hours (with `shred` to make sure it doesn't leave
|
||||||
|
anything on the disk). The logs include *only* the following information:
|
||||||
|
|
||||||
|
- Request time
|
||||||
|
- Requested host
|
||||||
|
- Requested path
|
||||||
|
- HTTP request method
|
||||||
|
- HTTP response code
|
||||||
|
|
||||||
|
This is the minimal information I need to trace any issues if something goes wrong, which is the main reason why I use
|
||||||
|
logging at all, to make it easier to find any issues.
|
||||||
|
|
||||||
|
## Data removal
|
||||||
|
If you want to remove any of your data from my server, [you can send me an email](mailto:ngn@ngn.tf). And yes this includes
|
||||||
|
removing usage metrics and logs.
|
4
doc/docs/privacy.tr.json
Normal file
4
doc/docs/privacy.tr.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"title": "Gizlilik",
|
||||||
|
"desc": "Gizliliğinize nasıl önem verdiğimi öğrenin"
|
||||||
|
}
|
40
doc/docs/privacy.tr.md
Normal file
40
doc/docs/privacy.tr.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
Kişisel olarak ben de bir gizlik savunucusu olduğumdan, bir yandan sunucumu güvende tutarken bir yandan da gizliliğinize önem
|
||||||
|
göstermek için elimden geleni yapıyorum. Aynı zamanda bu tarz şeyler hakkında şeffaf ve açık olmanın önemli olduğunu düşünüyorum,
|
||||||
|
o yüzden verilerinizi nasıl işlediğimi ya da depoladığımı anlamanız için bu dökümanı yazmaya karar verdim.
|
||||||
|
|
||||||
|
## DNS & SSL
|
||||||
|
Şuan cloudflare'in isim sunucularını kullanıyorum, ancak cloudflare alan adıma sahip değil (alan adımı cloudflare'den almadım)
|
||||||
|
ve aynı şekilde herhangi bir trafiğe vekillik etmiyor. Tüm DNS kayıtlarım *Sadece DNS* modunu kullanıyor, yani sadece
|
||||||
|
DNS kayıtlarından ibaretler ve benim sunucuma işaret ediyorlar, cloudflare'e değil. Bu aynı zamanda cloudflare SSL sertifikalarımı
|
||||||
|
kontrol etmiyor demek. Tüm sertifikalar benim sunucumda tutuluyor ve Let's Encrypt ile oluşturuldular. Yani sertifikalar bana ait
|
||||||
|
ve cloudflare'in aniden DNS kayıtlarını değiştirmesi mümkün değil (bu SSL'in bozulmasına sebep olur).
|
||||||
|
|
||||||
|
## Kullanım metrikleri
|
||||||
|
Sunucumda herhangi bir istek ya da trafik monitörlermesi yok. Yani hayır, HTTP(S) istekleriniz ya da diğer ağ
|
||||||
|
bağlantılarınız renki grafikler, pasta grafikleri gibi şeyler üretmek için işlenmiyor.
|
||||||
|
|
||||||
|
Bu sayfanın altında bir ziyaretçi sayısı takipçisi olduğunu farketmiş olabilirsiniz. Bu kullandığım tek kullanım/ziyaretçi
|
||||||
|
metrik takibi ve websitemin, özgür olan, bu yüzden kodunu kendiniz denetleyebileceğiniz API'ı ile implemente edildi.
|
||||||
|
|
||||||
|
Bu metrik takipçisinin, HTTP(S) istekleriniz hakkında herhangi bir veriyi bir veri tabanına kaydetmediğini belirtmek isterim.
|
||||||
|
Bu takipçi geçici olarak IP adresinizin SHA1 hash'ini bellekte tutuyor, bunun amacı aynı ziyaretçiyi sayfayı yenilediği zaman
|
||||||
|
ya da kısa bir süre için websitesini birden fazla kez ziyaret ettiği zaman tekrar saymayı önlemek. Belirli bir miktar istekten
|
||||||
|
sonra, IP adresinizin SHA1 hash'i bellekten kaldırılacaktır ve yeni bir ziyaretçinin SHA1'i onun yerine geçicektir.
|
||||||
|
|
||||||
|
## Kayıtlar
|
||||||
|
Tüm HTTP(S) servisleri nginx ile vekilleniyor, ve nginx hepsini disk üzerindeki bir dosyaya kaydediyor. Bu dosya (`access.log`)
|
||||||
|
sadece root kullanıcısı tarafından okunabilir, ve içerği her 4 saatde bir siliniyor (diskde veri kalmadığından emin olmak için
|
||||||
|
shred komutu ile). Kayıtlar *sadece* aşağıdaki bilgileri içeriyor:
|
||||||
|
|
||||||
|
- İstek zamanı
|
||||||
|
- İstenilen host
|
||||||
|
- İstenilen yol
|
||||||
|
- HTTP istek yöntemi
|
||||||
|
- HTTP cevap kodu
|
||||||
|
|
||||||
|
Bu birşeyler yanlış giderse sorunları bulmak için ihtiyacım olan en az bilgi, kayıt tutmamın ana sebeplerinden bir tanesi
|
||||||
|
zaten bu, sorunları bulmayı kolaylaştırmak.
|
||||||
|
|
||||||
|
## Veri silimi
|
||||||
|
Sunucumdan herhangi bir verinizi kaldırmak isterseniz, [bana bir email gönderebilirsiniz](mailto:ngn@ngn.tf). Ve evet buna
|
||||||
|
kullanım metrikleri ve kayıtlar dahil.
|
19
doc/inc/config.h
Normal file
19
doc/inc/config.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
#define OPT_NAME_MAX 20
|
||||||
|
char name[20]; // option name
|
||||||
|
char *value; // option value
|
||||||
|
bool required; // is the option required (does it need to have a value)
|
||||||
|
} option_t;
|
||||||
|
|
||||||
|
typedef struct config {
|
||||||
|
option_t *options;
|
||||||
|
int32_t count;
|
||||||
|
} config_t;
|
||||||
|
|
||||||
|
bool config_load(config_t *conf);
|
||||||
|
char *config_get(config_t *conf, const char *name);
|
18
doc/inc/docs.h
Normal file
18
doc/inc/docs.h
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <linux/limits.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
DIR *dir;
|
||||||
|
util_file_t *file;
|
||||||
|
char name[NAME_MAX + 1];
|
||||||
|
char *lang;
|
||||||
|
} docs_t;
|
||||||
|
|
||||||
|
bool docs_init(docs_t *docs, char *dir);
|
||||||
|
char *docs_next(docs_t *docs, char *name, bool content);
|
||||||
|
void docs_reset(docs_t *docs);
|
||||||
|
void docs_free(docs_t *docs);
|
7
doc/inc/routes.h
Normal file
7
doc/inc/routes.h
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ctorm/ctorm.h>
|
||||||
|
|
||||||
|
void route_cors(ctorm_req_t *req, ctorm_res_t *res);
|
||||||
|
void route_list(ctorm_req_t *req, ctorm_res_t *res);
|
||||||
|
void route_get(ctorm_req_t *req, ctorm_res_t *res);
|
||||||
|
void route_notfound(ctorm_req_t *req, ctorm_res_t *res);
|
19
doc/inc/util.h
Normal file
19
doc/inc/util.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ctorm/ctorm.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *content;
|
||||||
|
uint64_t size;
|
||||||
|
} util_file_t;
|
||||||
|
|
||||||
|
#define util_toupper(str) \
|
||||||
|
for (char *c = str; *c != 0; c++) \
|
||||||
|
*c = toupper(*c)
|
||||||
|
uint64_t util_endswith(char *str, char *suf);
|
||||||
|
void util_send(ctorm_res_t *res, uint16_t code, cJSON *json);
|
||||||
|
util_file_t *util_file_load(int dirfd, char *path);
|
||||||
|
void util_file_free(util_file_t *file);
|
||||||
|
bool util_parse_doc_name(char *name, char **lang, const char *ext);
|
51
doc/src/config.c
Normal file
51
doc/src/config.c
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#include <ctorm/ctorm.h>
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
option_t options[] = {
|
||||||
|
{"host", "0.0.0.0:7003", true }, // host the server should listen on
|
||||||
|
{"docs_dir", "./docs", true }, // documentation directory
|
||||||
|
{"", NULL, false},
|
||||||
|
};
|
||||||
|
|
||||||
|
bool config_load(config_t *conf) {
|
||||||
|
bzero(conf, sizeof(*conf));
|
||||||
|
|
||||||
|
char name_env[OPT_NAME_MAX + 10], name_copy[OPT_NAME_MAX], *value = NULL;
|
||||||
|
conf->options = options;
|
||||||
|
|
||||||
|
for (option_t *opt = conf->options; opt->value != NULL; opt++, conf->count++) {
|
||||||
|
strcpy(name_copy, opt->name);
|
||||||
|
util_toupper(name_copy);
|
||||||
|
snprintf(name_env, sizeof(name_env), "WEBSITE_%s", name_copy);
|
||||||
|
|
||||||
|
if ((value = getenv(name_env)) != NULL)
|
||||||
|
opt->value = value;
|
||||||
|
|
||||||
|
if (*opt->value == 0)
|
||||||
|
opt->value = NULL;
|
||||||
|
|
||||||
|
if (!opt->required || NULL != opt->value)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ctorm_fail("please specify a value for the required config option: %s (%s)", opt->name, name_env);
|
||||||
|
errno = EFAULT;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *config_get(config_t *conf, const char *name) {
|
||||||
|
for (int32_t i = 0; i < conf->count; i++)
|
||||||
|
if (strcmp(conf->options[i].name, name) == 0)
|
||||||
|
return conf->options[i].value;
|
||||||
|
return NULL;
|
||||||
|
}
|
106
doc/src/docs.c
Normal file
106
doc/src/docs.c
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
#include <linux/limits.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "util.h"
|
||||||
|
#include "docs.h"
|
||||||
|
|
||||||
|
#define DOCS_LANG_CODE_LEN 2
|
||||||
|
|
||||||
|
bool __docs_parse_name(docs_t *docs, char *ext) {
|
||||||
|
// check the extension
|
||||||
|
uint64_t ext_pos = util_endswith(docs->name, ext);
|
||||||
|
|
||||||
|
if (ext_pos == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// example.en.json\0 => example.en\0json\0
|
||||||
|
docs->name[ext_pos] = 0;
|
||||||
|
|
||||||
|
// example.en\0json\0
|
||||||
|
// |
|
||||||
|
// `--- find this
|
||||||
|
for (docs->lang = docs->name; *docs->lang != 0 && *docs->lang != '.'; docs->lang++)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (*docs->lang != '.')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// example.en\0json\0 => example.en\0json\0
|
||||||
|
*docs->lang++ = 0;
|
||||||
|
|
||||||
|
// example\0en\0json
|
||||||
|
// | | |
|
||||||
|
// | | `--- ext_pos
|
||||||
|
// | `-- lang
|
||||||
|
// `-- name
|
||||||
|
return strlen(docs->lang) == DOCS_LANG_CODE_LEN && *docs->name != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void __docs_clean(docs_t *docs) {
|
||||||
|
if (NULL == docs->file)
|
||||||
|
return;
|
||||||
|
|
||||||
|
util_file_free(docs->file);
|
||||||
|
docs->file = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool docs_init(docs_t *docs, char *dir) {
|
||||||
|
if (NULL == docs || NULL == dir) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bzero(docs, sizeof(*docs));
|
||||||
|
return NULL != (docs->dir = opendir(dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
char *docs_next(docs_t *docs, char *name, bool content) {
|
||||||
|
if (NULL == docs) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct dirent *ent = NULL;
|
||||||
|
__docs_clean(docs);
|
||||||
|
|
||||||
|
while (NULL != (ent = readdir(docs->dir))) {
|
||||||
|
if (*ent->d_name == '.')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
strcpy(docs->name, ent->d_name);
|
||||||
|
|
||||||
|
if (!__docs_parse_name(docs, content ? ".md" : ".json"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (NULL == name || strncmp(docs->name, name, NAME_MAX) == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NULL == ent) {
|
||||||
|
errno = ENOENT;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NULL == (docs->file = util_file_load(dirfd(docs->dir), ent->d_name)))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
return docs->file->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
void docs_reset(docs_t *docs) {
|
||||||
|
if (NULL != docs)
|
||||||
|
rewinddir(docs->dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
void docs_free(docs_t *docs) {
|
||||||
|
if (NULL == docs)
|
||||||
|
return;
|
||||||
|
|
||||||
|
__docs_clean(docs);
|
||||||
|
closedir(docs->dir);
|
||||||
|
|
||||||
|
bzero(docs, sizeof(*docs));
|
||||||
|
}
|
41
doc/src/main.c
Normal file
41
doc/src/main.c
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#include <ctorm/ctorm.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "routes.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
ctorm_app_t *app = NULL;
|
||||||
|
ctorm_config_t app_config;
|
||||||
|
|
||||||
|
config_t conf;
|
||||||
|
char *host = NULL;
|
||||||
|
|
||||||
|
if (!config_load(&conf))
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
host = config_get(&conf, "host");
|
||||||
|
|
||||||
|
ctorm_config_new(&app_config);
|
||||||
|
app_config.disable_logging = true;
|
||||||
|
app = ctorm_app_new(&app_config);
|
||||||
|
|
||||||
|
// middlewares
|
||||||
|
MIDDLEWARE_ALL(app, "/*", route_cors);
|
||||||
|
MIDDLEWARE_ALL(app, "/*/*", route_cors);
|
||||||
|
MIDDLEWARE_ALL(app, "/*/*/*", route_cors);
|
||||||
|
|
||||||
|
// routes
|
||||||
|
GET(app, "/list", route_list);
|
||||||
|
GET(app, "/get/:name", route_get);
|
||||||
|
|
||||||
|
ctorm_app_all(app, route_notfound);
|
||||||
|
ctorm_app_local(app, "config", &conf);
|
||||||
|
|
||||||
|
ctorm_info("starting the web server on %s", host);
|
||||||
|
|
||||||
|
if (!ctorm_app_run(app, host))
|
||||||
|
ctorm_fail("failed to start the app: %s", ctorm_geterror());
|
||||||
|
|
||||||
|
ctorm_app_free(app);
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
9
doc/src/routes/cors.c
Normal file
9
doc/src/routes/cors.c
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#include <ctorm/ctorm.h>
|
||||||
|
|
||||||
|
void route_cors(ctorm_req_t *req, ctorm_res_t *res) {
|
||||||
|
RES_SET("Access-Control-Allow-Origin", "*");
|
||||||
|
RES_SET("Access-Control-Allow-Headers",
|
||||||
|
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, "
|
||||||
|
"X-Requested-With");
|
||||||
|
RES_SET("Access-Control-Allow-Methods", "PUT, DELETE, GET");
|
||||||
|
}
|
70
doc/src/routes/get.c
Normal file
70
doc/src/routes/get.c
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#include <cjson/cJSON.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <linux/limits.h>
|
||||||
|
#include <ctorm/ctorm.h>
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "routes.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "docs.h"
|
||||||
|
|
||||||
|
void route_get(ctorm_req_t *req, ctorm_res_t *res) {
|
||||||
|
config_t *conf = REQ_LOCAL("config");
|
||||||
|
char *doc_name = REQ_PARAM("name");
|
||||||
|
char *docs_dir = config_get(conf, "docs_dir"), *doc_data = NULL;
|
||||||
|
cJSON *json = NULL, *doc_json = NULL;
|
||||||
|
docs_t docs;
|
||||||
|
|
||||||
|
if (NULL == doc_name) {
|
||||||
|
ctorm_fail("documentation name not specified (how did that even happend)");
|
||||||
|
util_send(res, 500, NULL);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!docs_init(&docs, docs_dir)) {
|
||||||
|
ctorm_fail("docs_init failed: %s", ctorm_geterror());
|
||||||
|
util_send(res, 500, NULL);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NULL == (json = cJSON_CreateObject())) {
|
||||||
|
ctorm_fail("failed to create cJSON object");
|
||||||
|
util_send(res, 500, NULL);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (NULL != (doc_data = docs_next(&docs, doc_name, false))) {
|
||||||
|
if (NULL == (doc_json = cJSON_Parse(doc_data))) {
|
||||||
|
ctorm_fail("failed to parse JSON: %s (%s)", docs.name, docs.lang);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(doc_json, "content", "");
|
||||||
|
cJSON_AddItemToObject(json, docs.lang, doc_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NULL == doc_json) {
|
||||||
|
util_send(res, 404, NULL);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
docs_reset(&docs);
|
||||||
|
|
||||||
|
while (NULL != (doc_data = docs_next(&docs, doc_name, true))) {
|
||||||
|
if (NULL == (doc_json = cJSON_GetObjectItem(json, docs.lang)))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
cJSON_DeleteItemFromObject(doc_json, "content");
|
||||||
|
cJSON_AddStringToObject(doc_json, "content", doc_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
util_send(res, 200, json);
|
||||||
|
|
||||||
|
end:
|
||||||
|
docs_free(&docs);
|
||||||
|
if (NULL != json)
|
||||||
|
cJSON_Delete(json);
|
||||||
|
}
|
53
doc/src/routes/list.c
Normal file
53
doc/src/routes/list.c
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#include <linux/limits.h>
|
||||||
|
#include <cjson/cJSON.h>
|
||||||
|
#include <ctorm/ctorm.h>
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "routes.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "docs.h"
|
||||||
|
|
||||||
|
void route_list(ctorm_req_t *req, ctorm_res_t *res) {
|
||||||
|
config_t *conf = REQ_LOCAL("config");
|
||||||
|
char *docs_dir = config_get(conf, "docs_dir"), *doc_data = NULL;
|
||||||
|
cJSON *array = NULL, *json = NULL, *doc_json = NULL;
|
||||||
|
docs_t docs;
|
||||||
|
|
||||||
|
if (!docs_init(&docs, docs_dir)) {
|
||||||
|
ctorm_fail("docs_init failed: %s", ctorm_geterror());
|
||||||
|
util_send(res, 500, NULL);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NULL == (json = cJSON_CreateObject())) {
|
||||||
|
ctorm_fail("failed to create cJSON object");
|
||||||
|
util_send(res, 500, NULL);
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (NULL != (doc_data = docs_next(&docs, NULL, false))) {
|
||||||
|
if (NULL == (array = cJSON_GetObjectItem(json, docs.lang)) &&
|
||||||
|
NULL == (array = cJSON_AddArrayToObject(json, docs.lang))) {
|
||||||
|
ctorm_fail("failed to create an array object for the language %s", docs.lang);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NULL == (doc_json = cJSON_Parse(doc_data))) {
|
||||||
|
ctorm_fail("failed to parse JSON: %s (%s)", docs.name, docs.lang);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(doc_json, "name", docs.name);
|
||||||
|
cJSON_AddItemToArray(array, doc_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
util_send(res, 200, json);
|
||||||
|
|
||||||
|
end:
|
||||||
|
docs_free(&docs);
|
||||||
|
if (NULL != json)
|
||||||
|
cJSON_Delete(json);
|
||||||
|
}
|
6
doc/src/routes/notfound.c
Normal file
6
doc/src/routes/notfound.c
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#include "routes.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
void route_notfound(ctorm_req_t *req, ctorm_res_t *res) {
|
||||||
|
return util_send(res, 404, NULL);
|
||||||
|
}
|
101
doc/src/util.c
Normal file
101
doc/src/util.c
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#include <linux/limits.h>
|
||||||
|
#include <ctorm/ctorm.h>
|
||||||
|
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
uint64_t util_endswith(char *str, char *suf) {
|
||||||
|
if (NULL == str || NULL == suf) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t sufl = strlen(suf);
|
||||||
|
uint64_t strl = strlen(str);
|
||||||
|
|
||||||
|
if (sufl > strl)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint64_t indx = strl - sufl;
|
||||||
|
|
||||||
|
return strncmp(str + indx, suf, sufl) == 0 ? indx : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void util_send(ctorm_res_t *res, uint16_t code, cJSON *json) {
|
||||||
|
if (NULL == json)
|
||||||
|
json = cJSON_CreateObject();
|
||||||
|
const char *error = "";
|
||||||
|
|
||||||
|
switch (code) {
|
||||||
|
case 404:
|
||||||
|
error = "not found";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 400:
|
||||||
|
error = "bad request";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 500:
|
||||||
|
error = "internal server error";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*error != 0)
|
||||||
|
cJSON_AddStringToObject(json, "error", error);
|
||||||
|
|
||||||
|
RES_JSON(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
util_file_t *util_file_load(int dirfd, char *path) {
|
||||||
|
if (NULL == path) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
util_file_t *file = NULL;
|
||||||
|
struct stat buf;
|
||||||
|
int fd = -1;
|
||||||
|
|
||||||
|
if (NULL == (file = malloc(sizeof(util_file_t))))
|
||||||
|
goto end; // errno set by malloc
|
||||||
|
|
||||||
|
if ((fd = openat(dirfd, path, O_RDONLY)) < 0)
|
||||||
|
goto end; // errno set by open
|
||||||
|
|
||||||
|
if (fstat(fd, &buf) < 0)
|
||||||
|
goto end; // errno set by fstat
|
||||||
|
|
||||||
|
if (NULL == (file->content = mmap(0, (file->size = buf.st_size), PROT_READ, MAP_PRIVATE, fd, 0)))
|
||||||
|
goto end; // errno set by mmap
|
||||||
|
|
||||||
|
end:
|
||||||
|
if (fd != -1)
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
if (NULL == file->content) {
|
||||||
|
free(file);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
void util_file_free(util_file_t *file) {
|
||||||
|
if (NULL == file)
|
||||||
|
return;
|
||||||
|
|
||||||
|
munmap(file->content, file->size);
|
||||||
|
free(file);
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# redeployment script for docker-compose
|
|
||||||
docker-compose down
|
|
||||||
docker rmi website_app:latest
|
|
||||||
docker rmi website_api:latest
|
|
||||||
git pull && docker-compose up -d
|
|
Loading…
x
Reference in New Issue
Block a user