diff --git a/README.md b/README.md
index d210924..013ef8a 100644
--- a/README.md
+++ b/README.md
@@ -6,9 +6,6 @@
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))
-> [!IMPORTANT]
-> I do not accept PRs as this is just my personal project.
-
## Directory structure
### `app`
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/)
### `api`
-Contains the API server, written in Go. It uses the [Fiber](https://github.com/gofiber/fiber) web
-framework which offers an [Express](https://expressjs.com/) like experience. I choose Fiber since I've used worked with express a lot in the past. However previously the I was using
+Contains the API server, written in Go. It uses the [Fiber](https://github.com/gofiber/fiber) web
+framework which offers an [Express](https://expressjs.com/) like experience. I choose Fiber since I've used worked with express a lot in the past. However previously the I was using
[Gin](https://github.com/gin-gonic/gin) (see history section).
-API stores all the data in a local sqlite(3) database. Go doesn't support sqlite3 out of the box so
+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).
### `admin`
-The frontend application does not contain an admin interface, I do the administration stuff (such as
-adding posts, adding services etc.) using the python script in this directory. This script can be
-installed on to the PATH by running the `install.sh` script. After installation it can be used
+The frontend application does not contain an admin interface, I do the administration stuff (such as
+adding posts, adding services etc.) using the python script in this directory. This script can be
+installed on to the PATH by running the `install.sh` script. After installation it can be used
by running `admin_script`.
## 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:
```yaml
version: "3"
@@ -38,8 +35,8 @@ services:
app:
build:
context: ./app
- args:
- API_URL: "https://api.ngn.tf"
+ environment:
+ - API_URL: "https://api.ngn.tf"
ports:
- "127.0.0.1:7002:3000"
depends_on:
@@ -48,8 +45,9 @@ services:
api:
build:
context: ./api
- args:
- PASSWORD: "securepassword"
+ environment:
+ - API_PASSWORD: "securepassword"
+ - API_FRONTEND_URL: "https://ngn.tf"
ports:
- "127.0.0.1:7001:7001"
volumes:
@@ -59,34 +57,38 @@ services:
## History
Some nostalgic history/changelog stuff (just for the major version numbers):
-- **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was just a simple HTML/CSS page,
-I never published any of the source code and I wiped the local copy on my USB drive in early 2022.
+- **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was just a simple HTML/CSS page,
+I never published any of the source code and I wiped the local copy on my USB drive in early 2022
-- **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,
-at the time svelte did not support SSR. I do not remember writting an API for it so I guess I just
-updated it everytime I wanted to add content? It was pretty much like a static website and was hosted
-on `ngn13.fun` as at this point I had my own hosting. The source code for this website was in a
-deleted github repository of mine, I looked for a local copy on my old hard drive but I wasn't able
+at the time svelte did not support SSR. I do not remember writting an API for it so I guess I just
+updated it everytime I wanted to add content? It was pretty much like a static website and was hosted
+on `ngn13.fun` as at this point I had my own hosting. The source code for this website was in a
+deleted github repository of mine, I looked for a local copy on my old hard drive but I wasn't able
to find it. I also do not remember how it looked like, sooo this version is pretty much lost :(
-- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework
-my website one more time in NuxtJS. I had really "fun time" using vue stuff. As NuxtJS supported
+- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework
+my website one more time in NuxtJS. I had really "fun time" using vue stuff. As NuxtJS supported
server-side code, this website had its own built in API. This website was also hosted on `ngn13.fun`
-- **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.
-SvelteKit was really fun to work with and I got my new website done pretty quickly. (I don't wanna
-brag or something but I really imporeved the CSS/styling stuff ya know). I also wrote a new API
-with Go and Gin. I did not publish the source code for the API, its still on my local gitea
-server tho. This website was hosted on `ngn13.fun` as well.
+SvelteKit was really fun to work with and I got my new website done pretty quickly. (I don't wanna
+brag or something but I really imporeved the CSS/styling stuff ya know). I also wrote a new API
+with Go and Gin. I did not publish the source code for the API, its still on my local gitea
+server tho. This website was hosted on `ngn13.fun` as well
-- **v4.0 (2023 november -)** The current major version of my website. The frontend is still
-similar to 3.0, the big changes are in the API. I rewrote the API with Fiber. This version is
-hosted on `ngn.tf` which is my new domain name btw.
+- **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0,
+the big changes are in the API. I rewrote the API with Fiber. This version was the first version which is hosted on
+`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_blog.png)
diff --git a/api/Dockerfile b/api/Dockerfile
index be1ab05..2477a1b 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -7,17 +7,11 @@ COPY *.mod ./
COPY *.sum ./
COPY Makefile ./
COPY routes ./routes
-COPY global ./global
+COPY global ./config
COPY database ./database
COPY util ./util
EXPOSE 7001
RUN make
-ARG PASSWORD
-ENV PASSWORD $PASSWORD
-
-ARG FRONTEND_URL
-ENV FRONTEND_URL $FRONTEND_URL
-
ENTRYPOINT ["/app/server"]
diff --git a/api/Makefile b/api/Makefile
index 8c5137c..f6908ec 100644
--- a/api/Makefile
+++ b/api/Makefile
@@ -1,10 +1,10 @@
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 $@ .
test:
- FRONTEND_URL=http://localhost:5173/ PASSWORD=test ./server
+ API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./server
format:
gofmt -s -w .
diff --git a/api/config/config.go b/api/config/config.go
new file mode 100644
index 0000000..29d5d60
--- /dev/null
+++ b/api/config/config.go
@@ -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 ""
+}
diff --git a/api/database/database.go b/api/database/database.go
index 0388f24..2adb27c 100644
--- a/api/database/database.go
+++ b/api/database/database.go
@@ -2,18 +2,10 @@ package database
import (
"database/sql"
- "github.com/ngn13/website/api/global"
)
-type Type struct {
- Sql *sql.DB
- Votes []global.Vote
-}
-
-func (t *Type) Setup() error {
- t.Votes = []global.Vote{}
-
- _, err := t.Sql.Exec(`
+func Setup(db *sql.DB) error {
+ _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS posts(
id TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
@@ -29,7 +21,7 @@ func (t *Type) Setup() error {
return err
}
- _, err = t.Sql.Exec(`
+ _, err = db.Exec(`
CREATE TABLE IF NOT EXISTS services(
name TEXT NOT NULL UNIQUE,
desc TEXT NOT NULL,
@@ -37,19 +29,16 @@ func (t *Type) Setup() error {
);
`)
- return err
-}
-
-func (t *Type) Open(p string) error {
- var err error
-
- if t.Sql, err = sql.Open("sqlite3", p); err != nil {
+ if err != nil {
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() {
- t.Sql.Close()
+ return err
}
diff --git a/api/database/post.go b/api/database/post.go
new file mode 100644
index 0000000..479277b
--- /dev/null
+++ b/api/database/post.go
@@ -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
+}
diff --git a/api/database/service.go b/api/database/service.go
new file mode 100644
index 0000000..3b14c71
--- /dev/null
+++ b/api/database/service.go
@@ -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
+}
diff --git a/api/database/vote.go b/api/database/vote.go
new file mode 100644
index 0000000..109cc27
--- /dev/null
+++ b/api/database/vote.go
@@ -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
+}
diff --git a/api/global/global.go b/api/global/global.go
deleted file mode 100644
index 6542187..0000000
--- a/api/global/global.go
+++ /dev/null
@@ -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
-}
diff --git a/api/main.go b/api/main.go
index 545ef4d..4751ce6 100644
--- a/api/main.go
+++ b/api/main.go
@@ -1,27 +1,40 @@
package main
import (
- "log"
- "net/http"
+ "database/sql"
"github.com/gofiber/fiber/v2"
+ "github.com/ngn13/website/api/config"
"github.com/ngn13/website/api/database"
"github.com/ngn13/website/api/routes"
+ "github.com/ngn13/website/api/util"
)
+var db *sql.DB
+
func main() {
var (
app *fiber.App
- db database.Type
+ //db *sql.DB
err error
)
- if err = db.Open("data.db"); err != nil {
- log.Fatalf("Cannot access the database: %s", err.Error())
+ if !config.Load() {
+ 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
}
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{
DisableStartupMessage: true,
})
@@ -31,7 +44,9 @@ func main() {
c.Set("Access-Control-Allow-Headers",
"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.Locals("database", &db)
+
return c.Next()
})
@@ -42,37 +57,46 @@ func main() {
// blog routes
blog_routes := app.Group("/blog")
- blog_routes.Get("/feed.atom", routes.GetAtomFeed)
- blog_routes.Get("/feed.rss", routes.GetRSSFeed)
- blog_routes.Get("/feed.json", routes.GetJSONFeed)
- blog_routes.Get("/sum", routes.SumPost)
- blog_routes.Get("/get", routes.GetPost)
- blog_routes.Get("/vote/set", routes.VoteSet)
- blog_routes.Get("/vote/status", routes.VoteStat)
+
+ // blog feed routes
+ blog_routes.Get("/feed.*", routes.GET_Feed)
+
+ // blog post routes
+ blog_routes.Get("/sum", routes.GET_PostSum)
+ 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 := app.Group("services")
- service_routes.Get("/all", routes.GetServices)
+ service_routes.Get("/all", routes.GET_Services)
// admin routes
admin_routes := app.Group("admin")
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 {
- return c.Status(http.StatusNotFound).JSON(fiber.Map{
- "error": "Requested endpoint not found",
- })
+ return util.ErrNotFound(c)
})
- 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 {
- log.Printf("Error starting the webserver: %s", err.Error())
+ util.Fail("error starting the webserver: %s", err.Error())
}
}
diff --git a/api/routes/admin.go b/api/routes/admin.go
index b628d52..c0f0451 100644
--- a/api/routes/admin.go
+++ b/api/routes/admin.go
@@ -1,16 +1,15 @@
package routes
import (
- "log"
+ "database/sql"
"net/http"
- "os"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/mattn/go-sqlite3"
+ "github.com/ngn13/website/api/config"
"github.com/ngn13/website/api/database"
- "github.com/ngn13/website/api/global"
"github.com/ngn13/website/api/util"
)
@@ -28,14 +27,12 @@ func AuthMiddleware(c *fiber.Ctx) error {
return c.Next()
}
-func Login(c *fiber.Ctx) error {
- if c.Query("pass") != os.Getenv("PASSWORD") {
- return c.Status(http.StatusUnauthorized).JSON(fiber.Map{
- "error": "Authentication failed",
- })
+func GET_Login(c *fiber.Ctx) error {
+ if c.Query("pass") != config.Get("password") {
+ return util.ErrAuth(c)
}
- 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{
"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()
- log.Printf("Logout from %s", util.GetIP(c))
+ util.Info("logout from %s", util.GetIP(c))
return c.Status(http.StatusOK).JSON(fiber.Map{
"error": "",
})
}
-func RemoveService(c *fiber.Ctx) error {
+func DEL_RemoveService(c *fiber.Ctx) error {
var (
- db *database.Type
- name string
+ db *sql.DB
+ 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")
if name == "" {
util.ErrBadData(c)
}
- _, err := db.Sql.Exec("DELETE FROM services WHERE name = ?", name)
- if util.ErrorCheck(err, c) {
+ if found, err = service.Get(db, name); err != nil {
+ 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.NoError(c)
}
-func AddService(c *fiber.Ctx) error {
+func PUT_AddService(c *fiber.Ctx) error {
var (
- service global.Service
- db *database.Type
+ service database.Service
+ db *sql.DB
+ found bool
+ err error
)
- db = c.Locals("database").(*database.Type)
+ db = *(c.Locals("database").(**sql.DB))
if c.BodyParser(&service) != nil {
return util.ErrBadJSON(c)
@@ -90,58 +101,63 @@ func AddService(c *fiber.Ctx) error {
return util.ErrBadData(c)
}
- rows, err := db.Sql.Query("SELECT * FROM services WHERE name = ?", service.Name)
- if util.ErrorCheck(err, c) {
+ if found, err = service.Get(db, service.Name); err != nil {
+ util.Fail("error while searching for a service (\"%s\"): %s", service.Name, err.Error())
return util.ErrServer(c)
}
- if rows.Next() {
- rows.Close()
- return util.ErrExists(c)
+ if found {
+ return util.ErrEntryExists(c)
}
- rows.Close()
-
- _, err = db.Sql.Exec(
- "INSERT INTO services(name, desc, url) values(?, ?, ?)",
- service.Name, service.Desc, service.Url,
- )
-
- if util.ErrorCheck(err, c) {
+ if err = service.Save(db); err != nil {
+ util.Fail("error while saving a new service (\"%s\"): %s", service.Name, err.Error())
return util.ErrServer(c)
}
return util.NoError(c)
}
-func RemovePost(c *fiber.Ctx) error {
+func DEL_RemovePost(c *fiber.Ctx) error {
var (
- db *database.Type
- id string
+ db *sql.DB
+ id string
+ found bool
+ err error
+ post database.Post
)
- db = c.Locals("database").(*database.Type)
- id = c.Query("id")
+ db = *(c.Locals("database").(**sql.DB))
- if id == "" {
+ if id = c.Query("id"); id == "" {
return util.ErrBadData(c)
}
- _, err := db.Sql.Exec("DELETE FROM posts WHERE id = ?", id)
- if util.ErrorCheck(err, c) {
+ if found, err = post.Get(db, id); err != nil {
+ util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error())
+ return util.ErrServer(c)
+ }
+
+ if !found {
+ return util.ErrEntryNotExists(c)
+ }
+
+ if err = post.Remove(db); err != nil {
+ util.Fail("error while removing a post (\"%s\"): %s", post.ID, err.Error())
return util.ErrServer(c)
}
return util.NoError(c)
}
-func AddPost(c *fiber.Ctx) error {
+func PUT_AddPost(c *fiber.Ctx) error {
var (
- db *database.Type
- post global.Post
+ db *sql.DB
+ post database.Post
+ err error
)
- db = c.Locals("database").(*database.Type)
+ db = *(c.Locals("database").(**sql.DB))
post.Public = 1
if c.BodyParser(&post) != nil {
@@ -153,19 +169,14 @@ func AddPost(c *fiber.Ctx) error {
}
post.Date = time.Now().Format("02/01/06")
- post.ID = util.TitleToID(post.Title)
- _, err := db.Sql.Exec(
- "INSERT INTO posts(id, title, author, date, content, public, vote) values(?, ?, ?, ?, ?, ?, ?)",
- 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 err = post.Save(db); err != nil && strings.Contains(err.Error(), sqlite3.ErrConstraintUnique.Error()) {
+ return util.ErrEntryExists(c)
}
- if util.ErrorCheck(err, c) {
- return util.ErrExists(c)
+ if err != nil {
+ util.Fail("error while saving a new post (\"%s\"): %s", post.ID, err.Error())
+ return util.ErrServer(c)
}
return util.NoError(c)
diff --git a/api/routes/blog.go b/api/routes/blog.go
index de214ed..c4e988f 100644
--- a/api/routes/blog.go
+++ b/api/routes/blog.go
@@ -3,164 +3,40 @@ package routes
import (
"database/sql"
"fmt"
- "net/http"
"net/url"
- "os"
+ "path"
+ "strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gorilla/feeds"
+ "github.com/ngn13/website/api/config"
"github.com/ngn13/website/api/database"
- "github.com/ngn13/website/api/global"
"github.com/ngn13/website/api/util"
)
-func PostFromRow(post *global.Post, rows *sql.Rows) 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 {
+func GET_Post(c *fiber.Ctx) error {
var (
+ post database.Post
id string
- to string
- voted bool
- db *database.Type
+ db *sql.DB
+ found bool
+ err error
)
- db = c.Locals("database").(*database.Type)
- id = c.Query("id")
- to = c.Query("to")
- voted = false
+ db = *(c.Locals("database").(**sql.DB))
- if id == "" || (to != "upvote" && to != "downvote") {
+ if id = c.Query("id"); 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 && db.Votes[i].Status == to {
- 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) {
+ if found, err = post.Get(db, id); err != nil {
+ util.Fail("error while search for a post (\"%s\"): %s", id, err.Error())
return util.ErrServer(c)
}
- for i := 0; i < len(db.Votes); i++ {
- if db.Votes[i].Client == util.GetIP(c) && db.Votes[i].Post == id && db.Votes[i].Status != to {
- 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))
+ if !found {
+ return util.ErrEntryNotExists(c)
}
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 (
- posts []global.Post
- post global.Post
- db *database.Type
+ posts []database.Post
+ rows *sql.Rows
+ db *sql.DB
err error
)
- db = c.Locals("database").(*database.Type)
+ db = *(c.Locals("database").(**sql.DB))
- rows, err := db.Sql.Query("SELECT * FROM posts")
- if util.ErrorCheck(err, c) {
+ if rows, err = db.Query("SELECT * FROM posts"); err != nil {
+ util.Fail("cannot load posts: %s", err.Error())
return util.ErrServer(c)
}
+ defer rows.Close()
for rows.Next() {
- 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)
}
@@ -201,7 +79,6 @@ func SumPost(c *fiber.Ctx) error {
posts = append(posts, post)
}
- rows.Close()
return c.JSON(fiber.Map{
"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 (
- posts []global.Post
- post global.Post
+ posts []database.Post
err error
)
- rows, err := db.Sql.Query("SELECT * FROM posts")
+ rows, err := db.Query("SELECT * FROM posts")
if err != nil {
return nil, err
}
for rows.Next() {
- err := PostFromRow(&post, rows)
+ var post database.Post
- if err != nil {
+ if err = post.Load(rows); err != nil {
return nil, err
}
@@ -236,7 +112,10 @@ func GetFeed(db *database.Type) (*feeds.Feed, error) {
}
rows.Close()
- blogurl, err := url.JoinPath(os.Getenv("FRONTEND_URL"), "/blog")
+ blogurl, err := url.JoinPath(
+ config.Get("frontend_url"), "/blog",
+ )
+
if err != nil {
return nil, fmt.Errorf("failed to create the blog URL: %s", err.Error())
}
@@ -273,46 +152,52 @@ func GetFeed(db *database.Type) (*feeds.Feed, error) {
return feed, nil
}
-func GetAtomFeed(c *fiber.Ctx) error {
- feed, err := GetFeed(c.Locals("database").(*database.Type))
- if util.ErrorCheck(err, c) {
+func GET_Feed(c *fiber.Ctx) error {
+ var (
+ db *sql.DB
+ err error
+ feed *feeds.Feed
+ name []string
+ res string
+ ext string
+ )
+
+ db = *(c.Locals("database").(**sql.DB))
+
+ if name = strings.Split(path.Base(c.Path()), "."); len(name) != 2 {
+ return util.ErrNotFound(c)
+ }
+ ext = name[1]
+
+ if feed, err = getFeed(db); err != nil {
+ util.Fail("cannot obtain the feed: %s", err.Error())
return util.ErrServer(c)
}
- atom, err := feed.ToAtom()
- if util.ErrorCheck(err, c) {
+ switch ext {
+ case "atom":
+ res, err = feed.ToAtom()
+ c.Set("Content-Type", "application/atom+xml")
+ break
+
+ case "json":
+ res, err = feed.ToJSON()
+ c.Set("Content-Type", "application/feed+json")
+ break
+
+ case "rss":
+ res, err = feed.ToRss()
+ c.Set("Content-Type", "application/rss+xml")
+ break
+
+ default:
+ return util.ErrNotFound(c)
+ }
+
+ if err != nil {
+ util.Fail("cannot obtain the feed as the specified format: %s", err.Error())
return util.ErrServer(c)
}
- c.Set("Content-Type", "application/atom+xml")
- 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))
+ return c.Send([]byte(res))
}
diff --git a/api/routes/services.go b/api/routes/services.go
index 3ab7469..0ded696 100644
--- a/api/routes/services.go
+++ b/api/routes/services.go
@@ -1,39 +1,40 @@
package routes
import (
- "log"
+ "database/sql"
"github.com/gofiber/fiber/v2"
"github.com/ngn13/website/api/database"
- "github.com/ngn13/website/api/global"
"github.com/ngn13/website/api/util"
)
-func GetServices(c *fiber.Ctx) error {
+func GET_Services(c *fiber.Ctx) error {
var (
- services []global.Service = []global.Service{}
- service global.Service
- db *database.Type
+ services []database.Service
+ rows *sql.Rows
+ db *sql.DB
err error
)
- db = c.Locals("database").(*database.Type)
+ db = *(c.Locals("database").(**sql.DB))
- rows, err := db.Sql.Query("SELECT * FROM services")
- if util.ErrorCheck(err, c) {
+ if rows, err = db.Query("SELECT * FROM services"); err != nil {
+ util.Fail("cannot load services: %s", err.Error())
return util.ErrServer(c)
}
+ defer rows.Close()
for rows.Next() {
- if err = rows.Scan(&service.Name, &service.Desc, &service.Url); err != nil {
- log.Println("Error scaning services row: " + err.Error())
- continue
+ var service database.Service
+
+ if err = service.Load(rows); err != nil {
+ util.Fail("error while loading service: %s", err.Error())
+ return util.ErrServer(c)
}
+
services = append(services, service)
}
- rows.Close()
-
return c.JSON(fiber.Map{
"error": "",
"result": services,
diff --git a/api/routes/vote.go b/api/routes/vote.go
new file mode 100644
index 0000000..be718a0
--- /dev/null
+++ b/api/routes/vote.go
@@ -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)
+}
diff --git a/api/util/log.go b/api/util/log.go
new file mode 100644
index 0000000..3d540c7
--- /dev/null
+++ b/api/util/log.go
@@ -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
+)
diff --git a/api/util/utils.go b/api/util/utils.go
index b4c30d8..61e1376 100644
--- a/api/util/utils.go
+++ b/api/util/utils.go
@@ -1,7 +1,8 @@
package util
import (
- "log"
+ "crypto/sha512"
+ "fmt"
"math/rand"
"net/http"
"strings"
@@ -9,6 +10,11 @@ import (
"github.com/gofiber/fiber/v2"
)
+func GetSHA512(s string) string {
+ hasher := sha512.New()
+ return fmt.Sprintf("%x", hasher.Sum([]byte(s)))
+}
+
func TitleToID(name string) string {
return strings.ToLower(strings.ReplaceAll(name, " ", ""))
}
@@ -21,15 +27,6 @@ func CreateToken() string {
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 {
return fiber.Map{
"error": error,
@@ -48,10 +45,14 @@ func ErrServer(c *fiber.Ctx) 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"))
}
+func ErrEntryNotExists(c *fiber.Ctx) error {
+ return c.Status(http.StatusNotFound).JSON(ErrorJSON("Entry does not exist"))
+}
+
func ErrBadData(c *fiber.Ctx) error {
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Provided data is invalid"))
}
@@ -64,6 +65,10 @@ func ErrAuth(c *fiber.Ctx) error {
return c.Status(http.StatusUnauthorized).JSON(ErrorJSON("Authentication failed"))
}
+func ErrNotFound(c *fiber.Ctx) error {
+ return c.Status(http.StatusNotFound).JSON(ErrorJSON("Requested endpoint not found"))
+}
+
func NoError(c *fiber.Ctx) error {
return c.Status(http.StatusOK).JSON(ErrorJSON(""))
}
diff --git a/app/Dockerfile b/app/Dockerfile
index 76e5dd8..8ca2f9a 100644
--- a/app/Dockerfile
+++ b/app/Dockerfile
@@ -1,10 +1,9 @@
FROM node:22.8.0 as build
WORKDIR /app
-COPY . /app
+COPY . /app
-ARG API_URL
-ENV VITE_API_URL_DEV $API_URL
+ENV VITE_API_URL_DEV http://placeholder/
RUN npm install && npm run build
@@ -19,4 +18,5 @@ COPY --from=build /app/package-lock.json ./package-lock.json
EXPOSE 4173
RUN bun install
-CMD ["bun", "build/index.js"]
+
+CMD ["./docker/entry.sh"]
diff --git a/app/docker/entry.sh b/app/docker/entry.sh
new file mode 100644
index 0000000..3e01376
--- /dev/null
+++ b/app/docker/entry.sh
@@ -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
diff --git a/app/package.json b/app/package.json
index 6b0f018..35ef5b4 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,6 +1,6 @@
{
"name": "website",
- "version": "4.8.0",
+ "version": "5.0.0",
"private": true,
"scripts": {
"dev": "VITE_API_URL_DEV=http://127.0.0.1:7001 vite dev",
diff --git a/app/src/lib/card.svelte b/app/src/lib/card.svelte
index 0b760ee..5cfb435 100644
--- a/app/src/lib/card.svelte
+++ b/app/src/lib/card.svelte
@@ -30,11 +30,12 @@
background: var(--dark-three);
box-shadow: var(--box-shadow);
border-radius: var(--radius);
+ border: solid 1px var(--border-color);
}
.title {
background: var(--dark-two);
- padding: 30px;
+ 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;
@@ -43,8 +44,7 @@
.content {
background: var(--dark-three);
- padding: 40px;
- padding-top: 30px;
+ padding: 30px;
color: white;
border-radius: 5px;
font-size: 25px;
diff --git a/app/src/lib/card_link.svelte b/app/src/lib/card_link.svelte
index d697664..2d94d07 100644
--- a/app/src/lib/card_link.svelte
+++ b/app/src/lib/card_link.svelte
@@ -42,6 +42,7 @@ a {
cursor: pointer;
transition: .4s;
text-decoration: none;
+ border: solid 1px var(--border-color);
}
a:hover > .title {
@@ -51,7 +52,7 @@ a:hover > .title {
.title {
border: solid 1px var(--dark-two);
background: var(--dark-two);
- padding: 30px;
+ 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;
@@ -60,7 +61,7 @@ a:hover > .title {
.content {
background: var(--dark-three);
- padding: 40px;
+ padding: 30px;
padding-top: 30px;
color: white;
border-radius: 5px;
diff --git a/app/src/lib/navbar.svelte b/app/src/lib/navbar.svelte
index 6679618..88785a0 100644
--- a/app/src/lib/navbar.svelte
+++ b/app/src/lib/navbar.svelte
@@ -11,7 +11,7 @@
home
services
blog
- donate
+
status
@@ -19,7 +19,7 @@