This commit is contained in:
ngn
2023-11-12 17:43:23 +03:00
parent 8c1552d639
commit 498a54cd20
68 changed files with 1983 additions and 301 deletions

2
api/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
server
api.db

13
api/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM golang:1.21.3
WORKDIR /app
COPY *.go ./
COPY *.mod ./
COPY *.sum ./
COPY Makefile ./
COPY routes ./routes
COPY util ./util
RUN make
ENTRYPOINT ["/app/server"]

7
api/Makefile Normal file
View File

@ -0,0 +1,7 @@
server: *.go routes/* util/*
go build -o server .
test:
PASSWORD=test ./server
.PHONY: test

19
api/go.mod Normal file
View File

@ -0,0 +1,19 @@
module github.com/ngn13/website/api
go 1.21.3
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/gofiber/fiber/v2 v2.50.0 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-sqlite3 v1.14.18 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.50.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.13.0 // indirect
)

29
api/go.sum Normal file
View File

@ -0,0 +1,29 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw=
github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

39
api/main.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"database/sql"
"log"
"github.com/gofiber/fiber/v2"
"github.com/ngn13/website/api/routes"
)
func CorsMiddleware(c *fiber.Ctx) error {
c.Set("Access-Control-Allow-Origin", "*")
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")
return c.Next()
}
func main() {
app := fiber.New()
app.Use(CorsMiddleware)
db, err := sql.Open("sqlite3", "data.db")
if err != nil {
log.Fatal("Cannot connect to the database: "+err.Error())
}
log.Println("Creating tables")
routes.BlogDb(db)
routes.ServicesDb(db)
routes.Setup(app, db)
log.Println("Starting web server at port 7001")
err = app.Listen("127.0.0.1:7001")
if err != nil {
log.Printf("Error starting the webserver: %s", err.Error())
}
defer db.Close()
}

141
api/routes/admin.go Normal file
View File

@ -0,0 +1,141 @@
package routes
import (
"log"
"net/http"
"os"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/mattn/go-sqlite3"
"github.com/ngn13/website/api/util"
)
var Token string = util.CreateToken()
func AuthMiddleware(c *fiber.Ctx) error {
if c.Path() == "/admin/login" {
return c.Next()
}
if c.Get("Authorization") != Token {
return util.ErrAuth(c)
}
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",
})
}
return c.Status(http.StatusOK).JSON(fiber.Map{
"error": "",
"token": Token,
})
}
func Logout(c *fiber.Ctx) error{
Token = util.CreateToken()
return c.Status(http.StatusOK).JSON(fiber.Map{
"error": "",
})
}
func RemoveService(c *fiber.Ctx) error {
name := c.Query("name")
if name == "" {
util.ErrBadData(c)
}
_, err := DB.Exec("DELETE FROM services WHERE name = ?", name)
if util.ErrorCheck(err, c){
return util.ErrServer(c)
}
return util.NoError(c)
}
func AddService(c *fiber.Ctx) error {
var service Service
if c.BodyParser(&service) != nil {
return util.ErrBadJSON(c)
}
if service.Name == "" || service.Desc == "" || service.Url == "" {
return util.ErrBadData(c)
}
rows, err := DB.Query("SELECT * FROM services WHERE name = ?", service.Name)
if util.ErrorCheck(err, c){
return util.ErrServer(c)
}
if rows.Next() {
rows.Close()
return util.ErrExists(c)
}
rows.Close()
_, err = DB.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.NoError(c)
}
func RemovePost(c *fiber.Ctx) error{
var id = c.Query("id")
if id == "" {
return util.ErrBadData(c)
}
_, err := DB.Exec("DELETE FROM posts WHERE id = ?", id)
if util.ErrorCheck(err, c){
return util.ErrServer(c)
}
return util.NoError(c)
}
func AddPost(c *fiber.Ctx) error{
var post Post
post.Public = 1
if c.BodyParser(&post) != nil {
return util.ErrBadJSON(c)
}
if post.Title == "" || post.Author == "" || post.Content == "" {
return util.ErrBadData(c)
}
post.Date = time.Now().Format("02/01/06")
log.Println(post.Date)
post.ID = TitleToID(post.Title)
_, err := DB.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 util.ErrorCheck(err, c){
return util.ErrExists(c)
}
return util.NoError(c)
}

162
api/routes/blog.go Normal file
View File

@ -0,0 +1,162 @@
package routes
import (
"database/sql"
"log"
"net/http"
"github.com/gofiber/fiber/v2"
"github.com/ngn13/website/api/util"
)
func BlogDb(db *sql.DB) {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS posts(
id TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
author TEXT NOT NULL,
date TEXT NOT NULL,
content TEXT NOT NULL,
public INTEGER NOT NULL,
vote INTEGER NOT NULL
);
`)
DB = db
if err != nil {
log.Fatal("Error creating table: "+err.Error())
}
}
func GetIP(c *fiber.Ctx) string {
if c.Get("X-Real-IP") != "" {
return c.Get("X-Real-IP")
}
return c.IP()
}
func VoteStat(c *fiber.Ctx) error{
var id = c.Query("id")
if id == "" {
return util.ErrBadData(c)
}
for i := 0; i < len(votelist); i++ {
if votelist[i].Client == GetIP(c) && votelist[i].Post == id {
return c.JSON(fiber.Map {
"error": "",
"result": votelist[i].Status,
})
}
}
return c.Status(http.StatusNotFound).JSON(util.ErrorJSON("Client never voted"))
}
func VoteSet(c *fiber.Ctx) error{
var id = c.Query("id")
var to = c.Query("to")
voted := false
if id == "" || (to != "upvote" && to != "downvote") {
return util.ErrBadData(c)
}
for i := 0; i < len(votelist); i++ {
if votelist[i].Client == GetIP(c) && votelist[i].Post == id && votelist[i].Status == to {
return c.Status(http.StatusForbidden).JSON(util.ErrorJSON("Client already voted"))
}
if votelist[i].Client == GetIP(c) && votelist[i].Post == id && votelist[i].Status != to {
voted = true
}
}
post, msg := GetPostByID(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.Exec("UPDATE posts SET vote = ? WHERE title = ?", vote, post.Title)
if util.ErrorCheck(err, c){
return util.ErrServer(c)
}
for i := 0; i < len(votelist); i++ {
if votelist[i].Client == GetIP(c) && votelist[i].Post == id && votelist[i].Status != to {
votelist[i].Status = to
return util.NoError(c)
}
}
var entry = Vote{}
entry.Client = GetIP(c)
entry.Status = to
entry.Post = id
votelist = append(votelist, entry)
return util.NoError(c)
}
func GetPost(c *fiber.Ctx) error{
var id = c.Query("id")
if id == "" {
return util.ErrBadData(c)
}
post, msg := GetPostByID(id)
if msg != ""{
return c.Status(http.StatusNotFound).JSON(util.ErrorJSON(msg))
}
return c.JSON(fiber.Map {
"error": "",
"result": post,
})
}
func SumPost(c *fiber.Ctx) error{
var posts []Post = []Post{}
rows, err := DB.Query("SELECT * FROM posts")
if util.ErrorCheck(err, c) {
return util.ErrServer(c)
}
for rows.Next() {
var post Post
err := PostFromRow(&post, rows)
if util.ErrorCheck(err, c) {
return util.ErrServer(c)
}
if post.Public == 0 {
continue
}
if len(post.Content) > 255{
post.Content = post.Content[0:250]
}
posts = append(posts, post)
}
rows.Close()
return c.JSON(fiber.Map {
"error": "",
"result": posts,
})
}

74
api/routes/global.go Normal file
View File

@ -0,0 +1,74 @@
package routes
import (
"database/sql"
"strings"
)
// ############### BLOG ###############
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"`
}
var votelist = []Vote{}
type Vote struct {
Post string
Client string
Status string
}
func PostFromRow(post *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(id string) (Post, string) {
var post Post = Post{}
post.Title = "NONE"
rows, err := DB.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 TitleToID(name string) string{
return strings.ToLower(strings.ReplaceAll(name, " ", ""))
}
// ############### SERVICES ###############
type Service struct {
Name string `json:"name"`
Desc string `json:"desc"`
Url string `json:"url"`
}

45
api/routes/routes.go Normal file
View File

@ -0,0 +1,45 @@
package routes
import (
"database/sql"
"net/http"
"github.com/gofiber/fiber/v2"
)
var DB *sql.DB
func Setup(app *fiber.App, db *sql.DB){
// database init
DB = db
// index route
app.Get("/", func(c *fiber.Ctx) error {
return c.Send([]byte("o/"))
})
// blog routes
app.Get("/blog/sum", SumPost)
app.Get("/blog/get", GetPost)
app.Get("/blog/vote/set", VoteSet)
app.Get("/blog/vote/status", VoteStat)
// service routes
app.Get("/services/all", GetServices)
// admin routes
app.Use("/admin*", AuthMiddleware)
app.Get("/admin/login", Login)
app.Get("/admin/logout", Logout)
app.Put("/admin/service/add", AddService)
app.Delete("/admin/service/remove", RemoveService)
app.Put("/admin/blog/add", AddPost)
app.Delete("/admin/blog/remove", RemovePost)
// 404 page
app.All("*", func(c *fiber.Ctx) error {
return c.Status(http.StatusNotFound).JSON(fiber.Map {
"error": "Requested endpoint not found",
})
})
}

48
api/routes/services.go Normal file
View File

@ -0,0 +1,48 @@
package routes
import (
"database/sql"
"log"
"github.com/gofiber/fiber/v2"
"github.com/ngn13/website/api/util"
)
func ServicesDb(db *sql.DB) {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS services(
name TEXT NOT NULL UNIQUE,
desc TEXT NOT NULL,
url TEXT NOT NULL
);
`)
if err != nil {
log.Fatal("Error creating table: "+err.Error())
}
}
func GetServices(c *fiber.Ctx) error {
var services []Service = []Service{}
rows, err := DB.Query("SELECT * FROM services")
if util.ErrorCheck(err, c) {
return util.ErrServer(c)
}
for rows.Next() {
var service Service
err := rows.Scan(&service.Name, &service.Desc, &service.Url)
if err != nil {
log.Println("Error scaning services row: "+err.Error())
continue
}
services = append(services, service)
}
rows.Close()
return c.JSON(fiber.Map {
"error": "",
"result": services,
})
}

60
api/util/utils.go Normal file
View File

@ -0,0 +1,60 @@
package util
import (
"log"
"math/rand"
"net/http"
"github.com/gofiber/fiber/v2"
)
var charlist []rune = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
func CreateToken() string {
b := make([]rune, 20)
for i := range b {
b[i] = charlist[rand.Intn(len(charlist))]
}
return string(b)
}
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,
}
}
func ErrServer(c *fiber.Ctx) error {
return c.Status(http.StatusInternalServerError).JSON(ErrorJSON("Server error"))
}
func ErrExists(c *fiber.Ctx) error {
return c.Status(http.StatusConflict).JSON(ErrorJSON("Entry already exists"))
}
func ErrBadData(c *fiber.Ctx) error {
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Provided data is invalid"))
}
func ErrBadJSON(c *fiber.Ctx) error {
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Bad JSON data"))
}
func ErrAuth(c *fiber.Ctx) error {
return c.Status(http.StatusUnauthorized).JSON(ErrorJSON("Authentication failed"))
}
func NoError(c *fiber.Ctx) error {
return c.Status(http.StatusOK).JSON(ErrorJSON(""))
}