v5.0 cleanup

This commit is contained in:
ngn 2024-10-06 17:30:25 +03:00
parent 38f51935b9
commit d5e875ae53
29 changed files with 711 additions and 427 deletions

View File

@ -6,9 +6,6 @@
This repo contains all the source code for my personal website, [ngn.tf](https://ngn.tf) This repo contains all the source code for my personal website, [ngn.tf](https://ngn.tf)
All code is licensed under AGPL version 3 (see [LICENSE.txt](LICENSE.txt)) All code is licensed under AGPL version 3 (see [LICENSE.txt](LICENSE.txt))
> [!IMPORTANT]
> I do not accept PRs as this is just my personal project.
## Directory structure ## Directory structure
### `app` ### `app`
Contains frontend application, written with SvelteKit. It supports full SSR. Contains frontend application, written with SvelteKit. It supports full SSR.
@ -16,21 +13,21 @@ Contains modified CSS from [github-markdown-css](https://github.com/sindresorhus
and fonts from [NerdFonts](https://www.nerdfonts.com/) 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 worked with express a lot in the past. However previously the I was using
[Gin](https://github.com/gin-gonic/gin) (see history section). [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).
### `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 posts, adding services etc.) using the python script in this directory. This script can be adding posts, adding services etc.) using the python script in this directory. This script can be
installed on to the PATH by running the `install.sh` script. After installation it can be used installed on to the PATH by running the `install.sh` 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. I have created a `compose.yml` file so the API and the
frontend application can be deployed easily with just the `docker-compose up` command: frontend application can be deployed easily with just the `docker-compose up` command:
```yaml ```yaml
version: "3" version: "3"
@ -38,8 +35,8 @@ services:
app: app:
build: build:
context: ./app context: ./app
args: environment:
API_URL: "https://api.ngn.tf" - API_URL: "https://api.ngn.tf"
ports: ports:
- "127.0.0.1:7002:3000" - "127.0.0.1:7002:3000"
depends_on: depends_on:
@ -48,8 +45,9 @@ services:
api: api:
build: build:
context: ./api context: ./api
args: environment:
PASSWORD: "securepassword" - API_PASSWORD: "securepassword"
- API_FRONTEND_URL: "https://ngn.tf"
ports: ports:
- "127.0.0.1:7001:7001" - "127.0.0.1:7001:7001"
volumes: volumes:
@ -59,34 +57,38 @@ services:
## 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
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png). - **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code
was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png)
- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework) - **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework)
my website with one of the fancy JS frameworks. I decided to go with Svelte. Not the kit version, my website with one of the fancy JS frameworks. I decided to go with Svelte. Not the kit version,
at the time svelte did not support SSR. I do not remember writting an API for it so I guess I just at the time svelte did not support SSR. I do not remember writting an API for it so I guess I just
updated it everytime I wanted to add content? It was pretty much like a static website and was hosted updated it everytime I wanted to add content? It was pretty much like a static website and was hosted
on `ngn13.fun` as at this point I had my own hosting. The source code for this website was in a on `ngn13.fun` as at this point I had my own hosting. The source code for this website was in a
deleted github repository of mine, I looked for a local copy on my old hard drive but I wasn't able 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 time in NuxtJS. I had really "fun time" using vue stuff. As NuxtJS supported my website one more time in NuxtJS. I had really "fun time" using vue stuff. As NuxtJS supported
server-side code, this website had its own built in API. This website was also hosted on `ngn13.fun` server-side code, this website had its own built in API. This website was also hosted on `ngn13.fun`
- **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 I was going with SvelteKit as I haven't had the greatest experience with NuxtJS. again, this time I was going with SvelteKit as I haven't had the greatest experience with NuxtJS.
SvelteKit was really fun to work with and I got my new website done pretty quickly. (I don't wanna SvelteKit was really fun to work with and I got my new website done pretty quickly. (I don't wanna
brag or something but I really imporeved the CSS/styling stuff ya know). I also wrote a new API brag or something but I really imporeved the CSS/styling stuff ya know). I also wrote a new API
with Go and Gin. I did not publish the source code for the API, its still on my local gitea with Go and Gin. I did not publish the source code for the API, its still on my local gitea
server tho. This website was hosted on `ngn13.fun` as well. server tho. This website was hosted on `ngn13.fun` as well
- **v4.0 (2023 november -)** The current major version of my website. The frontend is still - **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0,
similar to 3.0, the big changes are in the API. I rewrote the API with Fiber. This version is the big changes are in the API. I rewrote the API with Fiber. This version was the first version which is hosted on
hosted on `ngn.tf` which is my new domain name btw. `ngn.tf` which is my new domain name btw
## Screenshots - **v5.0 (2024 october - ...)**: The current major version of my website, has small UI and API tweaks when
compared to 4.0
## Screenshots (from v4.0)
![](assets/4.0_index.png) ![](assets/4.0_index.png)
![](assets/4.0_blog.png) ![](assets/4.0_blog.png)

View File

@ -7,17 +7,11 @@ COPY *.mod ./
COPY *.sum ./ COPY *.sum ./
COPY Makefile ./ COPY Makefile ./
COPY routes ./routes COPY routes ./routes
COPY global ./global COPY global ./config
COPY database ./database COPY database ./database
COPY util ./util COPY util ./util
EXPOSE 7001 EXPOSE 7001
RUN make RUN make
ARG PASSWORD
ENV PASSWORD $PASSWORD
ARG FRONTEND_URL
ENV FRONTEND_URL $FRONTEND_URL
ENTRYPOINT ["/app/server"] ENTRYPOINT ["/app/server"]

View File

@ -1,10 +1,10 @@
all: server all: server
server: *.go routes/*.go util/*.go global/*.go database/*.go server: *.go routes/*.go database/*.go util/*.go config/*.go
go build -o $@ . go build -o $@ .
test: test:
FRONTEND_URL=http://localhost:5173/ PASSWORD=test ./server API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./server
format: format:
gofmt -s -w . gofmt -s -w .

60
api/config/config.go Normal file
View File

@ -0,0 +1,60 @@
package config
import (
"fmt"
"os"
"strings"
"github.com/ngn13/website/api/util"
)
type Option struct {
Name string
Value string
Required bool
}
func (o *Option) Env() string {
return strings.ToUpper(fmt.Sprintf("API_%s", o.Name))
}
var options []Option = []Option{
{Name: "password", Value: "", Required: true},
{Name: "frontend_url", Value: "http://localhost:5173/", Required: true},
}
func Load() bool {
var val string
for i := range options {
if val = os.Getenv(options[i].Env()); val == "" {
continue
}
options[i].Value = val
options[i].Required = false
}
for i := range options {
if options[i].Required && options[i].Value == "" {
util.Fail("please specify the required config option \"%s\" (\"%s\")", options[i].Name, options[i].Env())
return false
}
if options[i].Required && options[i].Value != "" {
util.Fail("using the default value \"%s\" for required config option \"%s\" (\"%s\")", options[i].Value, options[i].Name, options[i].Env())
}
}
return true
}
func Get(name string) string {
for i := range options {
if options[i].Name != name {
continue
}
return options[i].Value
}
return ""
}

View File

@ -2,18 +2,10 @@ package database
import ( import (
"database/sql" "database/sql"
"github.com/ngn13/website/api/global"
) )
type Type struct { func Setup(db *sql.DB) error {
Sql *sql.DB _, err := db.Exec(`
Votes []global.Vote
}
func (t *Type) Setup() error {
t.Votes = []global.Vote{}
_, err := t.Sql.Exec(`
CREATE TABLE IF NOT EXISTS posts( CREATE TABLE IF NOT EXISTS posts(
id TEXT NOT NULL UNIQUE, id TEXT NOT NULL UNIQUE,
title TEXT NOT NULL, title TEXT NOT NULL,
@ -29,7 +21,7 @@ func (t *Type) Setup() error {
return err return err
} }
_, err = t.Sql.Exec(` _, err = db.Exec(`
CREATE TABLE IF NOT EXISTS services( CREATE TABLE IF NOT EXISTS services(
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
desc TEXT NOT NULL, desc TEXT NOT NULL,
@ -37,19 +29,16 @@ func (t *Type) Setup() error {
); );
`) `)
return err if err != nil {
}
func (t *Type) Open(p string) error {
var err error
if t.Sql, err = sql.Open("sqlite3", p); err != nil {
return err return err
} }
return t.Setup() _, err = db.Exec(`
} CREATE TABLE IF NOT EXISTS votes(
hash TEXT NOT NULL UNIQUE,
is_upvote INTEGER NOT NULL
);
`)
func (t *Type) Close() { return err
t.Sql.Close()
} }

71
api/database/post.go Normal file
View File

@ -0,0 +1,71 @@
package database
import (
"database/sql"
"github.com/ngn13/website/api/util"
)
type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Date string `json:"date"`
Content string `json:"content"`
Public int `json:"public"`
Vote int `json:"vote"`
}
func (p *Post) Load(rows *sql.Rows) error {
return rows.Scan(&p.ID, &p.Title, &p.Author, &p.Date, &p.Content, &p.Public, &p.Vote)
}
func (p *Post) Get(db *sql.DB, id string) (bool, error) {
var (
success bool
rows *sql.Rows
err error
)
if rows, err = db.Query("SELECT * FROM posts WHERE id = ?", id); err != nil {
return false, err
}
defer rows.Close()
if success = rows.Next(); !success {
return false, nil
}
if err = p.Load(rows); err != nil {
return false, err
}
return true, nil
}
func (p *Post) Remove(db *sql.DB) error {
_, err := db.Exec("DELETE FROM posts WHERE id = ?", p.ID)
return err
}
func (p *Post) Save(db *sql.DB) error {
p.ID = util.TitleToID(p.Title)
_, err := db.Exec(
"INSERT INTO posts(id, title, author, date, content, public, vote) values(?, ?, ?, ?, ?, ?, ?)",
p.ID, p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote,
)
return err
}
func (p *Post) Update(db *sql.DB) error {
p.ID = util.TitleToID(p.Title)
_, err := db.Exec(
"UPDATE posts SET title = ?, author = ?, date = ?, content = ?, public = ?, vote = ? WHERE id = ?",
p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote, p.ID,
)
return err
}

56
api/database/service.go Normal file
View File

@ -0,0 +1,56 @@
package database
import (
"database/sql"
)
type Service struct {
Name string `json:"name"`
Desc string `json:"desc"`
Url string `json:"url"`
}
func (s *Service) Load(rows *sql.Rows) error {
return rows.Scan(&s.Name, &s.Desc, &s.Url)
}
func (s *Service) Get(db *sql.DB, name string) (bool, error) {
var (
success bool
rows *sql.Rows
err error
)
if rows, err = db.Query("SELECT * FROM services WHERE name = ?", name); err != nil {
return false, err
}
defer rows.Close()
if success = rows.Next(); !success {
return false, nil
}
if err = s.Load(rows); err != nil {
return false, err
}
return true, nil
}
func (s *Service) Remove(db *sql.DB) error {
_, err := db.Exec(
"DELETE FROM services WHERE name = ?",
s.Name,
)
return err
}
func (s *Service) Save(db *sql.DB) error {
_, err := db.Exec(
"INSERT INTO services(name, desc, url) values(?, ?, ?)",
s.Name, s.Desc, s.Url,
)
return err
}

49
api/database/vote.go Normal file
View File

@ -0,0 +1,49 @@
package database
import "database/sql"
type Vote struct {
Hash string
IsUpvote bool
}
func (v *Vote) Load(rows *sql.Rows) error {
return rows.Scan(&v.Hash, &v.IsUpvote)
}
func (v *Vote) Get(db *sql.DB, hash string) (bool, error) {
var (
success bool
rows *sql.Rows
err error
)
if rows, err = db.Query("SELECT * FROM votes WHERE hash = ?", hash); err != nil {
return false, err
}
defer rows.Close()
if success = rows.Next(); !success {
return false, nil
}
if err = v.Load(rows); err != nil {
return false, err
}
return true, nil
}
func (v *Vote) Update(db *sql.DB) error {
_, err := db.Exec("UPDATE votes SET is_upvote = ? WHERE hash = ?", v.IsUpvote, v.Hash)
return err
}
func (v *Vote) Save(db *sql.DB) error {
_, err := db.Exec(
"INSERT INTO votes(hash, is_upvote) values(?, ?)",
v.Hash, v.IsUpvote,
)
return err
}

View File

@ -1,23 +0,0 @@
package global
type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Date string `json:"date"`
Content string `json:"content"`
Public int `json:"public"`
Vote int `json:"vote"`
}
type Service struct {
Name string `json:"name"`
Desc string `json:"desc"`
Url string `json:"url"`
}
type Vote struct {
Post string
Client string
Status string
}

View File

@ -1,27 +1,40 @@
package main package main
import ( import (
"log" "database/sql"
"net/http"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/ngn13/website/api/config"
"github.com/ngn13/website/api/database" "github.com/ngn13/website/api/database"
"github.com/ngn13/website/api/routes" "github.com/ngn13/website/api/routes"
"github.com/ngn13/website/api/util"
) )
var db *sql.DB
func main() { func main() {
var ( var (
app *fiber.App app *fiber.App
db database.Type //db *sql.DB
err error err error
) )
if err = db.Open("data.db"); err != nil { if !config.Load() {
log.Fatalf("Cannot access the database: %s", err.Error()) util.Fail("failed to load the configuration")
return
}
if db, err = sql.Open("sqlite3", "data.db"); err != nil {
util.Fail("cannot access the database: %s", err.Error())
return return
} }
defer db.Close() defer db.Close()
if err = database.Setup(db); err != nil {
util.Fail("cannot setup the database: %s", err.Error())
return
}
app = fiber.New(fiber.Config{ app = fiber.New(fiber.Config{
DisableStartupMessage: true, DisableStartupMessage: true,
}) })
@ -31,7 +44,9 @@ func main() {
c.Set("Access-Control-Allow-Headers", c.Set("Access-Control-Allow-Headers",
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET") c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET")
c.Locals("database", &db) c.Locals("database", &db)
return c.Next() return c.Next()
}) })
@ -42,37 +57,46 @@ func main() {
// blog routes // blog routes
blog_routes := app.Group("/blog") blog_routes := app.Group("/blog")
blog_routes.Get("/feed.atom", routes.GetAtomFeed)
blog_routes.Get("/feed.rss", routes.GetRSSFeed) // blog feed routes
blog_routes.Get("/feed.json", routes.GetJSONFeed) blog_routes.Get("/feed.*", routes.GET_Feed)
blog_routes.Get("/sum", routes.SumPost)
blog_routes.Get("/get", routes.GetPost) // blog post routes
blog_routes.Get("/vote/set", routes.VoteSet) blog_routes.Get("/sum", routes.GET_PostSum)
blog_routes.Get("/vote/status", routes.VoteStat) blog_routes.Get("/get", routes.GET_Post)
// blog post vote routes
blog_routes.Get("/vote/set", routes.GET_VoteSet)
blog_routes.Get("/vote/get", routes.GET_VoteGet)
// service routes // service routes
service_routes := app.Group("services") service_routes := app.Group("services")
service_routes.Get("/all", routes.GetServices) service_routes.Get("/all", routes.GET_Services)
// admin routes // admin routes
admin_routes := app.Group("admin") admin_routes := app.Group("admin")
admin_routes.Use("*", routes.AuthMiddleware) admin_routes.Use("*", routes.AuthMiddleware)
admin_routes.Get("/login", routes.Login)
admin_routes.Get("/logout", routes.Logout)
admin_routes.Put("/service/add", routes.AddService)
admin_routes.Delete("/service/remove", routes.RemoveService)
admin_routes.Put("/blog/add", routes.AddPost)
admin_routes.Delete("/blog/remove", routes.RemovePost)
// 404 routes // admin auth routes
admin_routes.Get("/login", routes.GET_Login)
admin_routes.Get("/logout", routes.GET_Logout)
// admin service managment routes
admin_routes.Put("/service/add", routes.PUT_AddService)
admin_routes.Delete("/service/remove", routes.DEL_RemoveService)
// admin blog managment routes
admin_routes.Put("/blog/add", routes.PUT_AddPost)
admin_routes.Delete("/blog/remove", routes.DEL_RemovePost)
// 404 route
app.All("*", func(c *fiber.Ctx) error { app.All("*", func(c *fiber.Ctx) error {
return c.Status(http.StatusNotFound).JSON(fiber.Map{ return util.ErrNotFound(c)
"error": "Requested endpoint not found",
})
}) })
log.Println("Starting web server at port 7001") util.Info("starting web server at port 7001")
if err = app.Listen("0.0.0.0:7001"); err != nil { if err = app.Listen("0.0.0.0:7001"); err != nil {
log.Printf("Error starting the webserver: %s", err.Error()) util.Fail("error starting the webserver: %s", err.Error())
} }
} }

View File

@ -1,16 +1,15 @@
package routes package routes
import ( import (
"log" "database/sql"
"net/http" "net/http"
"os"
"strings" "strings"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
"github.com/ngn13/website/api/config"
"github.com/ngn13/website/api/database" "github.com/ngn13/website/api/database"
"github.com/ngn13/website/api/global"
"github.com/ngn13/website/api/util" "github.com/ngn13/website/api/util"
) )
@ -28,14 +27,12 @@ func AuthMiddleware(c *fiber.Ctx) error {
return c.Next() return c.Next()
} }
func Login(c *fiber.Ctx) error { func GET_Login(c *fiber.Ctx) error {
if c.Query("pass") != os.Getenv("PASSWORD") { if c.Query("pass") != config.Get("password") {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{ return util.ErrAuth(c)
"error": "Authentication failed",
})
} }
log.Printf("New login from %s", util.GetIP(c)) util.Info("new login from %s", util.GetIP(c))
return c.Status(http.StatusOK).JSON(fiber.Map{ return c.Status(http.StatusOK).JSON(fiber.Map{
"error": "", "error": "",
@ -43,44 +40,58 @@ func Login(c *fiber.Ctx) error {
}) })
} }
func Logout(c *fiber.Ctx) error { func GET_Logout(c *fiber.Ctx) error {
Token = util.CreateToken() Token = util.CreateToken()
log.Printf("Logout from %s", util.GetIP(c)) util.Info("logout from %s", util.GetIP(c))
return c.Status(http.StatusOK).JSON(fiber.Map{ return c.Status(http.StatusOK).JSON(fiber.Map{
"error": "", "error": "",
}) })
} }
func RemoveService(c *fiber.Ctx) error { func DEL_RemoveService(c *fiber.Ctx) error {
var ( var (
db *database.Type db *sql.DB
name string service database.Service
name string
found bool
err error
) )
db = c.Locals("database").(*database.Type) db = *(c.Locals("database").(**sql.DB))
name = c.Query("name") name = c.Query("name")
if name == "" { if name == "" {
util.ErrBadData(c) util.ErrBadData(c)
} }
_, err := db.Sql.Exec("DELETE FROM services WHERE name = ?", name) if found, err = service.Get(db, name); err != nil {
if util.ErrorCheck(err, c) { util.Fail("error while searching for a service (\"%s\"): %s", name, err.Error())
return util.ErrServer(c)
}
if !found {
return util.ErrEntryNotExists(c)
}
if err = service.Remove(db); err != nil {
util.Fail("error while removing a service (\"%s\"): %s", service.Name, err.Error())
return util.ErrServer(c) return util.ErrServer(c)
} }
return util.NoError(c) return util.NoError(c)
} }
func AddService(c *fiber.Ctx) error { func PUT_AddService(c *fiber.Ctx) error {
var ( var (
service global.Service service database.Service
db *database.Type db *sql.DB
found bool
err error
) )
db = c.Locals("database").(*database.Type) db = *(c.Locals("database").(**sql.DB))
if c.BodyParser(&service) != nil { if c.BodyParser(&service) != nil {
return util.ErrBadJSON(c) return util.ErrBadJSON(c)
@ -90,58 +101,63 @@ func AddService(c *fiber.Ctx) error {
return util.ErrBadData(c) return util.ErrBadData(c)
} }
rows, err := db.Sql.Query("SELECT * FROM services WHERE name = ?", service.Name) if found, err = service.Get(db, service.Name); err != nil {
if util.ErrorCheck(err, c) { util.Fail("error while searching for a service (\"%s\"): %s", service.Name, err.Error())
return util.ErrServer(c) return util.ErrServer(c)
} }
if rows.Next() { if found {
rows.Close() return util.ErrEntryExists(c)
return util.ErrExists(c)
} }
rows.Close() if err = service.Save(db); err != nil {
util.Fail("error while saving a new service (\"%s\"): %s", service.Name, err.Error())
_, err = db.Sql.Exec(
"INSERT INTO services(name, desc, url) values(?, ?, ?)",
service.Name, service.Desc, service.Url,
)
if util.ErrorCheck(err, c) {
return util.ErrServer(c) return util.ErrServer(c)
} }
return util.NoError(c) return util.NoError(c)
} }
func RemovePost(c *fiber.Ctx) error { func DEL_RemovePost(c *fiber.Ctx) error {
var ( var (
db *database.Type db *sql.DB
id string id string
found bool
err error
post database.Post
) )
db = c.Locals("database").(*database.Type) db = *(c.Locals("database").(**sql.DB))
id = c.Query("id")
if id == "" { if id = c.Query("id"); id == "" {
return util.ErrBadData(c) return util.ErrBadData(c)
} }
_, err := db.Sql.Exec("DELETE FROM posts WHERE id = ?", id) if found, err = post.Get(db, id); err != nil {
if util.ErrorCheck(err, c) { util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error())
return util.ErrServer(c)
}
if !found {
return util.ErrEntryNotExists(c)
}
if err = post.Remove(db); err != nil {
util.Fail("error while removing a post (\"%s\"): %s", post.ID, err.Error())
return util.ErrServer(c) return util.ErrServer(c)
} }
return util.NoError(c) return util.NoError(c)
} }
func AddPost(c *fiber.Ctx) error { func PUT_AddPost(c *fiber.Ctx) error {
var ( var (
db *database.Type db *sql.DB
post global.Post post database.Post
err error
) )
db = c.Locals("database").(*database.Type) db = *(c.Locals("database").(**sql.DB))
post.Public = 1 post.Public = 1
if c.BodyParser(&post) != nil { if c.BodyParser(&post) != nil {
@ -153,19 +169,14 @@ func AddPost(c *fiber.Ctx) error {
} }
post.Date = time.Now().Format("02/01/06") post.Date = time.Now().Format("02/01/06")
post.ID = util.TitleToID(post.Title)
_, err := db.Sql.Exec( if err = post.Save(db); err != nil && strings.Contains(err.Error(), sqlite3.ErrConstraintUnique.Error()) {
"INSERT INTO posts(id, title, author, date, content, public, vote) values(?, ?, ?, ?, ?, ?, ?)", return util.ErrEntryExists(c)
post.ID, post.Title, post.Author, post.Date, post.Content, post.Public, post.Vote,
)
if err != nil && strings.Contains(err.Error(), sqlite3.ErrConstraintUnique.Error()) {
return util.ErrExists(c)
} }
if util.ErrorCheck(err, c) { if err != nil {
return util.ErrExists(c) util.Fail("error while saving a new post (\"%s\"): %s", post.ID, err.Error())
return util.ErrServer(c)
} }
return util.NoError(c) return util.NoError(c)

View File

@ -3,164 +3,40 @@ package routes
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"os" "path"
"strings"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gorilla/feeds" "github.com/gorilla/feeds"
"github.com/ngn13/website/api/config"
"github.com/ngn13/website/api/database" "github.com/ngn13/website/api/database"
"github.com/ngn13/website/api/global"
"github.com/ngn13/website/api/util" "github.com/ngn13/website/api/util"
) )
func PostFromRow(post *global.Post, rows *sql.Rows) error { func GET_Post(c *fiber.Ctx) error {
err := rows.Scan(&post.ID, &post.Title, &post.Author, &post.Date, &post.Content, &post.Public, &post.Vote)
if err != nil {
return err
}
return nil
}
func GetPostByID(db *database.Type, id string) (global.Post, string) {
var post global.Post = global.Post{}
post.Title = "NONE"
rows, err := db.Sql.Query("SELECT * FROM posts WHERE id = ?", id)
if err != nil {
return post, "Server error"
}
success := rows.Next()
if !success {
rows.Close()
return post, "Post not found"
}
err = PostFromRow(&post, rows)
if err != nil {
rows.Close()
return post, "Server error"
}
rows.Close()
if post.Title == "NONE" {
return post, "Post not found"
}
return post, ""
}
func VoteStat(c *fiber.Ctx) error {
var (
db *database.Type
id string
)
db = c.Locals("database").(*database.Type)
id = c.Query("id")
if id == "" {
return util.ErrBadData(c)
}
for i := 0; i < len(db.Votes); i++ {
if db.Votes[i].Client == util.GetIP(c) && db.Votes[i].Post == id {
return c.JSON(fiber.Map{
"error": "",
"result": db.Votes[i].Status,
})
}
}
return c.Status(http.StatusNotFound).JSON(util.ErrorJSON("Client never voted"))
}
func VoteSet(c *fiber.Ctx) error {
var ( var (
post database.Post
id string id string
to string db *sql.DB
voted bool found bool
db *database.Type err error
) )
db = c.Locals("database").(*database.Type) db = *(c.Locals("database").(**sql.DB))
id = c.Query("id")
to = c.Query("to")
voted = false
if id == "" || (to != "upvote" && to != "downvote") { if id = c.Query("id"); id == "" {
return util.ErrBadData(c) return util.ErrBadData(c)
} }
for i := 0; i < len(db.Votes); i++ { if found, err = post.Get(db, id); err != nil {
if db.Votes[i].Client == util.GetIP(c) && db.Votes[i].Post == id && db.Votes[i].Status == to { util.Fail("error while search for a post (\"%s\"): %s", id, err.Error())
return c.Status(http.StatusForbidden).JSON(util.ErrorJSON("Client already voted"))
}
if db.Votes[i].Client == util.GetIP(c) && db.Votes[i].Post == id && db.Votes[i].Status != to {
voted = true
}
}
post, msg := GetPostByID(db, id)
if msg != "" {
return c.Status(http.StatusNotFound).JSON(util.ErrorJSON(msg))
}
vote := post.Vote + 1
if to == "downvote" {
vote = post.Vote - 1
}
if to == "downvote" && voted {
vote = vote - 1
}
if to == "upvote" && voted {
vote = vote + 1
}
_, err := db.Sql.Exec("UPDATE posts SET vote = ? WHERE title = ?", vote, post.Title)
if util.ErrorCheck(err, c) {
return util.ErrServer(c) return util.ErrServer(c)
} }
for i := 0; i < len(db.Votes); i++ { if !found {
if db.Votes[i].Client == util.GetIP(c) && db.Votes[i].Post == id && db.Votes[i].Status != to { return util.ErrEntryNotExists(c)
db.Votes[i].Status = to
return util.NoError(c)
}
}
var entry = global.Vote{}
entry.Client = util.GetIP(c)
entry.Status = to
entry.Post = id
db.Votes = append(db.Votes, entry)
return util.NoError(c)
}
func GetPost(c *fiber.Ctx) error {
var (
id string
db *database.Type
)
id = c.Query("id")
db = c.Locals("database").(*database.Type)
if id == "" {
return util.ErrBadData(c)
}
post, msg := GetPostByID(db, id)
if msg != "" {
return c.Status(http.StatusNotFound).JSON(util.ErrorJSON(msg))
} }
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
@ -169,25 +45,27 @@ func GetPost(c *fiber.Ctx) error {
}) })
} }
func SumPost(c *fiber.Ctx) error { func GET_PostSum(c *fiber.Ctx) error {
var ( var (
posts []global.Post posts []database.Post
post global.Post rows *sql.Rows
db *database.Type db *sql.DB
err error err error
) )
db = c.Locals("database").(*database.Type) db = *(c.Locals("database").(**sql.DB))
rows, err := db.Sql.Query("SELECT * FROM posts") if rows, err = db.Query("SELECT * FROM posts"); err != nil {
if util.ErrorCheck(err, c) { util.Fail("cannot load posts: %s", err.Error())
return util.ErrServer(c) return util.ErrServer(c)
} }
defer rows.Close()
for rows.Next() { for rows.Next() {
err = PostFromRow(&post, rows) var post database.Post
if util.ErrorCheck(err, c) { if err = post.Load(rows); err != nil {
util.Fail("error while loading post: %s", err.Error())
return util.ErrServer(c) return util.ErrServer(c)
} }
@ -201,7 +79,6 @@ func SumPost(c *fiber.Ctx) error {
posts = append(posts, post) posts = append(posts, post)
} }
rows.Close()
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"error": "", "error": "",
@ -209,22 +86,21 @@ func SumPost(c *fiber.Ctx) error {
}) })
} }
func GetFeed(db *database.Type) (*feeds.Feed, error) { func getFeed(db *sql.DB) (*feeds.Feed, error) {
var ( var (
posts []global.Post posts []database.Post
post global.Post
err error err error
) )
rows, err := db.Sql.Query("SELECT * FROM posts") rows, err := db.Query("SELECT * FROM posts")
if err != nil { if err != nil {
return nil, err return nil, err
} }
for rows.Next() { for rows.Next() {
err := PostFromRow(&post, rows) var post database.Post
if err != nil { if err = post.Load(rows); err != nil {
return nil, err return nil, err
} }
@ -236,7 +112,10 @@ func GetFeed(db *database.Type) (*feeds.Feed, error) {
} }
rows.Close() rows.Close()
blogurl, err := url.JoinPath(os.Getenv("FRONTEND_URL"), "/blog") blogurl, err := url.JoinPath(
config.Get("frontend_url"), "/blog",
)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create the blog URL: %s", err.Error()) return nil, fmt.Errorf("failed to create the blog URL: %s", err.Error())
} }
@ -273,46 +152,52 @@ func GetFeed(db *database.Type) (*feeds.Feed, error) {
return feed, nil return feed, nil
} }
func GetAtomFeed(c *fiber.Ctx) error { func GET_Feed(c *fiber.Ctx) error {
feed, err := GetFeed(c.Locals("database").(*database.Type)) var (
if util.ErrorCheck(err, c) { db *sql.DB
err error
feed *feeds.Feed
name []string
res string
ext string
)
db = *(c.Locals("database").(**sql.DB))
if name = strings.Split(path.Base(c.Path()), "."); len(name) != 2 {
return util.ErrNotFound(c)
}
ext = name[1]
if feed, err = getFeed(db); err != nil {
util.Fail("cannot obtain the feed: %s", err.Error())
return util.ErrServer(c) return util.ErrServer(c)
} }
atom, err := feed.ToAtom() switch ext {
if util.ErrorCheck(err, c) { case "atom":
res, err = feed.ToAtom()
c.Set("Content-Type", "application/atom+xml")
break
case "json":
res, err = feed.ToJSON()
c.Set("Content-Type", "application/feed+json")
break
case "rss":
res, err = feed.ToRss()
c.Set("Content-Type", "application/rss+xml")
break
default:
return util.ErrNotFound(c)
}
if err != nil {
util.Fail("cannot obtain the feed as the specified format: %s", err.Error())
return util.ErrServer(c) return util.ErrServer(c)
} }
c.Set("Content-Type", "application/atom+xml") return c.Send([]byte(res))
return c.Send([]byte(atom))
}
func GetRSSFeed(c *fiber.Ctx) error {
feed, err := GetFeed(c.Locals("database").(*database.Type))
if util.ErrorCheck(err, c) {
return util.ErrServer(c)
}
rss, err := feed.ToRss()
if util.ErrorCheck(err, c) {
return util.ErrServer(c)
}
c.Set("Content-Type", "application/rss+xml")
return c.Send([]byte(rss))
}
func GetJSONFeed(c *fiber.Ctx) error {
feed, err := GetFeed(c.Locals("database").(*database.Type))
if util.ErrorCheck(err, c) {
return util.ErrServer(c)
}
json, err := feed.ToJSON()
if util.ErrorCheck(err, c) {
return util.ErrServer(c)
}
c.Set("Content-Type", "application/feed+json")
return c.Send([]byte(json))
} }

View File

@ -1,39 +1,40 @@
package routes package routes
import ( import (
"log" "database/sql"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/ngn13/website/api/database" "github.com/ngn13/website/api/database"
"github.com/ngn13/website/api/global"
"github.com/ngn13/website/api/util" "github.com/ngn13/website/api/util"
) )
func GetServices(c *fiber.Ctx) error { func GET_Services(c *fiber.Ctx) error {
var ( var (
services []global.Service = []global.Service{} services []database.Service
service global.Service rows *sql.Rows
db *database.Type db *sql.DB
err error err error
) )
db = c.Locals("database").(*database.Type) db = *(c.Locals("database").(**sql.DB))
rows, err := db.Sql.Query("SELECT * FROM services") if rows, err = db.Query("SELECT * FROM services"); err != nil {
if util.ErrorCheck(err, c) { util.Fail("cannot load services: %s", err.Error())
return util.ErrServer(c) return util.ErrServer(c)
} }
defer rows.Close()
for rows.Next() { for rows.Next() {
if err = rows.Scan(&service.Name, &service.Desc, &service.Url); err != nil { var service database.Service
log.Println("Error scaning services row: " + err.Error())
continue if err = service.Load(rows); err != nil {
util.Fail("error while loading service: %s", err.Error())
return util.ErrServer(c)
} }
services = append(services, service) services = append(services, service)
} }
rows.Close()
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"error": "", "error": "",
"result": services, "result": services,

139
api/routes/vote.go Normal file
View File

@ -0,0 +1,139 @@
package routes
import (
"database/sql"
"github.com/gofiber/fiber/v2"
"github.com/ngn13/website/api/database"
"github.com/ngn13/website/api/util"
)
func getVoteHash(id string, ip string) string {
return util.GetSHA512(id + "_" + ip)
}
func GET_VoteGet(c *fiber.Ctx) error {
var (
db *sql.DB
id string
hash string
vote database.Vote
found bool
err error
)
db = *(c.Locals("database").(**sql.DB))
if id = c.Query("id"); id == "" {
return util.ErrBadData(c)
}
hash = getVoteHash(id, util.GetIP(c))
if found, err = vote.Get(db, hash); err != nil {
util.Fail("error while searchig for a vote (\"%s\"): %s", hash, err.Error())
return util.ErrServer(c)
}
if !found {
return util.ErrEntryNotExists(c)
}
if vote.IsUpvote {
return c.JSON(fiber.Map{
"error": "",
"result": "upvote",
})
}
return c.JSON(fiber.Map{
"error": "",
"result": "downvote",
})
}
func GET_VoteSet(c *fiber.Ctx) error {
var (
db *sql.DB
id string
is_upvote bool
hash string
vote database.Vote
post database.Post
found bool
err error
)
db = *(c.Locals("database").(**sql.DB))
id = c.Query("id")
if c.Query("to") == "" || id == "" {
return util.ErrBadData(c)
}
if found, err = post.Get(db, id); err != nil {
util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error())
return util.ErrServer(c)
}
if !found {
return util.ErrEntryNotExists(c)
}
is_upvote = c.Query("to") == "upvote"
hash = getVoteHash(id, util.GetIP(c))
if found, err = vote.Get(db, hash); err != nil {
util.Fail("error while searching for a vote (\"%s\"): %s", hash, err.Error())
return util.ErrServer(c)
}
if found {
if vote.IsUpvote == is_upvote {
return util.ErrEntryExists(c)
}
if vote.IsUpvote && !is_upvote {
post.Vote -= 2
}
if !vote.IsUpvote && is_upvote {
post.Vote += 2
}
vote.IsUpvote = is_upvote
if err = post.Update(db); err != nil {
util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error())
return util.ErrServer(c)
}
if err = vote.Update(db); err != nil {
util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error())
return util.ErrServer(c)
}
return util.NoError(c)
}
vote.Hash = hash
vote.IsUpvote = is_upvote
if is_upvote {
post.Vote++
} else {
post.Vote--
}
if err = post.Update(db); err != nil {
util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error())
return util.ErrServer(c)
}
if err = vote.Save(db); err != nil {
util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error())
return util.ErrServer(c)
}
return util.NoError(c)
}

12
api/util/log.go Normal file
View File

@ -0,0 +1,12 @@
package util
import (
"log"
"os"
)
var (
Info = log.New(os.Stdout, "\033[34m[info]\033[0m ", log.Ltime|log.Lshortfile).Printf
Warn = log.New(os.Stderr, "\033[33m[warn]\033[0m ", log.Ltime|log.Lshortfile).Printf
Fail = log.New(os.Stderr, "\033[31m[fail]\033[0m ", log.Ltime|log.Lshortfile).Printf
)

View File

@ -1,7 +1,8 @@
package util package util
import ( import (
"log" "crypto/sha512"
"fmt"
"math/rand" "math/rand"
"net/http" "net/http"
"strings" "strings"
@ -9,6 +10,11 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
func GetSHA512(s string) string {
hasher := sha512.New()
return fmt.Sprintf("%x", hasher.Sum([]byte(s)))
}
func TitleToID(name string) string { func TitleToID(name string) string {
return strings.ToLower(strings.ReplaceAll(name, " ", "")) return strings.ToLower(strings.ReplaceAll(name, " ", ""))
} }
@ -21,15 +27,6 @@ func CreateToken() string {
return string(s) return string(s)
} }
func ErrorCheck(err error, c *fiber.Ctx) bool {
if err != nil {
log.Printf("Server error: '%s' on %s\n", err, c.Path())
return true
}
return false
}
func ErrorJSON(error string) fiber.Map { func ErrorJSON(error string) fiber.Map {
return fiber.Map{ return fiber.Map{
"error": error, "error": error,
@ -48,10 +45,14 @@ func ErrServer(c *fiber.Ctx) error {
return c.Status(http.StatusInternalServerError).JSON(ErrorJSON("Server error")) return c.Status(http.StatusInternalServerError).JSON(ErrorJSON("Server error"))
} }
func ErrExists(c *fiber.Ctx) error { func ErrEntryExists(c *fiber.Ctx) error {
return c.Status(http.StatusConflict).JSON(ErrorJSON("Entry already exists")) return c.Status(http.StatusConflict).JSON(ErrorJSON("Entry already exists"))
} }
func ErrEntryNotExists(c *fiber.Ctx) error {
return c.Status(http.StatusNotFound).JSON(ErrorJSON("Entry does not exist"))
}
func ErrBadData(c *fiber.Ctx) error { func ErrBadData(c *fiber.Ctx) error {
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Provided data is invalid")) return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Provided data is invalid"))
} }
@ -64,6 +65,10 @@ func ErrAuth(c *fiber.Ctx) error {
return c.Status(http.StatusUnauthorized).JSON(ErrorJSON("Authentication failed")) return c.Status(http.StatusUnauthorized).JSON(ErrorJSON("Authentication failed"))
} }
func ErrNotFound(c *fiber.Ctx) error {
return c.Status(http.StatusNotFound).JSON(ErrorJSON("Requested endpoint not found"))
}
func NoError(c *fiber.Ctx) error { func NoError(c *fiber.Ctx) error {
return c.Status(http.StatusOK).JSON(ErrorJSON("")) return c.Status(http.StatusOK).JSON(ErrorJSON(""))
} }

View File

@ -1,10 +1,9 @@
FROM node:22.8.0 as build FROM node:22.8.0 as build
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
ARG API_URL ENV VITE_API_URL_DEV http://placeholder/
ENV VITE_API_URL_DEV $API_URL
RUN npm install && npm run build RUN npm install && npm run build
@ -19,4 +18,5 @@ COPY --from=build /app/package-lock.json ./package-lock.json
EXPOSE 4173 EXPOSE 4173
RUN bun install RUN bun install
CMD ["bun", "build/index.js"]
CMD ["./docker/entry.sh"]

7
app/docker/entry.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/sh
# replace the API URL
find ./build -type f -exec sed "s/http:\/\/placeholder\//${API_URL//\//\\/}/g" -i "{}" \;
# start the application
bun build/index.js

View File

@ -1,6 +1,6 @@
{ {
"name": "website", "name": "website",
"version": "4.8.0", "version": "5.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "VITE_API_URL_DEV=http://127.0.0.1:7001 vite dev", "dev": "VITE_API_URL_DEV=http://127.0.0.1:7001 vite dev",

View File

@ -30,11 +30,12 @@
background: var(--dark-three); background: var(--dark-three);
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
border-radius: var(--radius); border-radius: var(--radius);
border: solid 1px var(--border-color);
} }
.title { .title {
background: var(--dark-two); background: var(--dark-two);
padding: 30px; padding: 25px;
border-radius: 7px 7px 0px 0px; border-radius: 7px 7px 0px 0px;
font-size: 20px; font-size: 20px;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
@ -43,8 +44,7 @@
.content { .content {
background: var(--dark-three); background: var(--dark-three);
padding: 40px; padding: 30px;
padding-top: 30px;
color: white; color: white;
border-radius: 5px; border-radius: 5px;
font-size: 25px; font-size: 25px;

View File

@ -42,6 +42,7 @@ a {
cursor: pointer; cursor: pointer;
transition: .4s; transition: .4s;
text-decoration: none; text-decoration: none;
border: solid 1px var(--border-color);
} }
a:hover > .title { a:hover > .title {
@ -51,7 +52,7 @@ a:hover > .title {
.title { .title {
border: solid 1px var(--dark-two); border: solid 1px var(--dark-two);
background: var(--dark-two); background: var(--dark-two);
padding: 30px; padding: 25px;
border-radius: 7px 7px 0px 0px; border-radius: 7px 7px 0px 0px;
font-size: 20px; font-size: 20px;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
@ -60,7 +61,7 @@ a:hover > .title {
.content { .content {
background: var(--dark-three); background: var(--dark-three);
padding: 40px; padding: 30px;
padding-top: 30px; padding-top: 30px;
color: white; color: white;
border-radius: 5px; border-radius: 5px;

View File

@ -11,7 +11,7 @@
<NavbarLink link="/">home</NavbarLink> <NavbarLink link="/">home</NavbarLink>
<NavbarLink link="/services">services</NavbarLink> <NavbarLink link="/services">services</NavbarLink>
<NavbarLink link="/blog">blog</NavbarLink> <NavbarLink link="/blog">blog</NavbarLink>
<NavbarLink link="/donate">donate</NavbarLink> <!-- <NavbarLink link="/donate">donate</NavbarLink> -->
<NavbarLink link="https://stats.ngn.tf">status</NavbarLink> <NavbarLink link="https://stats.ngn.tf">status</NavbarLink>
</div> </div>
</nav> </nav>
@ -19,7 +19,7 @@
<style> <style>
nav { nav {
background: var(--dark-one); background: var(--dark-one);
padding: 25px 30px 27px 25px; padding: 20px 26px 22px 20px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;

View File

@ -37,9 +37,9 @@ main {
background: var(--dark-two); background: var(--dark-two);
border-radius: var(--radius); border-radius: var(--radius);
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
border: solid 1px var(--border-color);
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border: none;
color: white; color: white;
gap: 100px; gap: 100px;
transition: .4s; transition: .4s;

View File

@ -23,7 +23,7 @@
<Card title="whoami"> <Card title="whoami">
<div class="whoami-box"> <div class="whoami-box">
<div class="whoami-pic"> <div class="whoami-pic">
<img alt="My profile pic" src="https://files.ngn.tf/pplow.png"> <img alt="My profile" src="https://files.ngn.tf/pplow.png">
<a href="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D"> <a href="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D">
<c><i class="nf nf-oct-key"></i> Keyoxide</c> <c><i class="nf nf-oct-key"></i> Keyoxide</c>
</a> </a>
@ -54,9 +54,9 @@
</Card> </Card>
<Card title="wall"> <Card title="wall">
Here are some links if you want to get in contact with me, please do not Here are some links if you want to get in contact with me, I highly
use these if you want to contact about the services that I offer, I have a prefer email and I usually respond to emails in 1 or 2 days, just make
seperate email for that: <a href="mailto:services@ngn.tf"><c>services@ngn.tf</c></a> sure to check your spam folder (turns out running a TOR relay gets your IP into multiple blacklists)
<ul> <ul>
<li> <li>
<c><i class="nf nf-cod-github"></i></c> <c><i class="nf nf-cod-github"></i></c>
@ -64,15 +64,15 @@
</li> </li>
<li> <li>
<c><i class="nf nf-md-mastodon"></i></c> <c><i class="nf nf-md-mastodon"></i></c>
<a href="https://mastodon.social/@ngn" rel="me">Mastodon</a> <a href="https://defcon.social/@ngn" rel="me">Mastodon</a>
</li> </li>
<li> <li>
<c><i class="nf nf-md-email"></i></c> <c><i class="nf nf-md-email"></i></c>
<a href="mailto:ngn@ngn.tf">Email (personal)</a> <a href="mailto:ngn@ngn.tf">Email</a>
</li> </li>
<li> <li>
<c><i class="nf nf-md-xmpp"></i></c> <c><i class="nf nf-md-xmpp"></i></c>
<a href="xmpp:ngn@chat.ngn.tf">XMPP (my preferred way of communication)</a> <a href="xmpp:ngn@chat.ngn.tf">XMPP</a>
</li> </li>
</ul> </ul>
</Card> </Card>
@ -80,20 +80,20 @@
</main> </main>
<div class="version"> <div class="version">
<p>v4.9</p> <p>v5.0</p>
</div> </div>
<style> <style>
main{ main{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 35px; gap: 28px;
padding: 50px; padding: 50px;
} }
.flexbox { .flexbox {
display: flex; display: flex;
gap: 30px; gap: 28px;
} }
.whoami-box { .whoami-box {
@ -116,10 +116,10 @@ main{
} }
.whoami-pic img { .whoami-pic img {
width: 250px; width: 200px;
border-radius: 30px; border-radius: 20px;
border: solid 2px white; border: solid 1px var(--border-color);
animation-name: fullBorderAnimation; animation-name: fullBorderAnimation;
animation-duration: 10s; animation-duration: 10s;
animation-iteration-count: infinite; animation-iteration-count: infinite;

View File

@ -47,7 +47,7 @@
.post-list{ .post-list{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 35px; gap: 28px;
} }
main { main {
@ -68,14 +68,15 @@ p {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 15px; gap: 10px;
} }
.feed-list a { .feed-list a {
text-decoration: none; text-decoration: none;
padding: 10px 18px 10px 18px; padding: 10px 15px 10px 15px;
background: var(--dark-three); background: var(--dark-three);
border-radius: var(--radius); border-radius: var(--radius);
border: solid 1px var(--border-color);
color: var(--white); color: var(--white);
font-size: 20px; font-size: 20px;
font-weight: 900; font-weight: 900;

View File

@ -15,7 +15,7 @@
let audio let audio
async function get_status() { async function get_status() {
const res = await fetch(api+"/blog/vote/status?id="+data.id) const res = await fetch(api+"/blog/vote/get?id="+data.id)
const json = await res.json() const json = await res.json()
if(json["error"]!= ""){ if(json["error"]!= ""){
@ -61,6 +61,7 @@
if (voted){ if (voted){
data.vote += 2 data.vote += 2
} }
else { else {
voted = true voted = true
data.vote += 1 data.vote += 1
@ -81,6 +82,7 @@
if (voted){ if (voted){
data.vote -= 2 data.vote -= 2
} }
else { else {
voted = true voted = true
data.vote -= 1 data.vote -= 1
@ -139,9 +141,10 @@ main {
} }
.content { .content {
padding: 35px; padding: 30px;
background: var(--dark-four); background: var(--dark-four);
border-radius: var(--radius); border-radius: var(--radius);
border: solid 1px var(--border-color);
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
width: auto; width: auto;
width: 100%; width: 100%;

View File

@ -19,7 +19,7 @@
<main> <main>
<Card title="bash donate.sh"> <Card title="bash donate.sh">
I work on free/libre and open source software and offer free services. General hosting I work on free/libre and open source software and offer free services. General hosting
and stuff costs around 550₺ (~$17), so feel free to donate in order to help me keep and stuff costs around 550₺ (~$17) per month, so feel free to donate in order to help me keep
everything up and running! everything up and running!
<table> <table>
<tr> <tr>
@ -35,6 +35,8 @@
</td> </td>
</tr> </tr>
</table> </table>
Also huge thanks to all of you who has donated so far, even if it's a small amount, I highly
appreciate it. Thank you!
</Card> </Card>
</main> </main>
@ -52,7 +54,7 @@ table {
color: white; color: white;
font-size: 20px; font-size: 20px;
width: 100%; width: 100%;
margin: 30px 0 0 0; margin: 30px 0 30px 0;
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
} }

View File

@ -32,9 +32,7 @@
<ul> <ul>
<li> <li>
<c><i class="nf nf-cod-account"></i> Registration:</c> All the services are offered for free, and all of them <c><i class="nf nf-cod-account"></i> Registration:</c> All the services are offered for free, and all of them
are accessiable to public. If you want to use a service that requires are accessiable to public. And registrations are open for the all services that support account registrations.
registration, you can email <a href="mailto:services@ngn.tf"><c>services@ngn.tf</c></a>
for an account.
</li> </li>
<li> <li>
<c><i class="nf nf-fa-eye_slash"></i> Privacy:</c> To protect user privacy, all the web proxy logs are cleared regularly. <c><i class="nf nf-fa-eye_slash"></i> Privacy:</c> To protect user privacy, all the web proxy logs are cleared regularly.
@ -64,7 +62,7 @@ main {
align-content: center; align-content: center;
justify-content: center; justify-content: center;
padding: 50px; padding: 50px;
gap: 30px; gap: 28px;
} }
.flexcol { .flexcol {
@ -72,7 +70,7 @@ main {
flex-direction: column; flex-direction: column;
align-content: center; align-content: center;
justify-content: center; justify-content: center;
gap: 20px; gap: 13px;
} }
.flexrow { .flexrow {
@ -81,11 +79,7 @@ main {
align-content: center; align-content: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
gap: 20px; gap: 13px;
}
a {
text-decoration: none;
} }
ul { ul {

View File

@ -17,6 +17,7 @@
*/ */
--text-shadow: 0px 10px 20px rgba(90, 90, 90, 0.8); --text-shadow: 0px 10px 20px rgba(90, 90, 90, 0.8);
--box-shadow: rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px; --box-shadow: rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px;
--border-color: #2f2f2f;
} }
* { * {