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 @@