v5.0 cleanup
This commit is contained in:
parent
38f51935b9
commit
d5e875ae53
68
README.md
68
README.md
@ -6,9 +6,6 @@
|
|||||||
This repo contains all the source code for my personal website, [ngn.tf](https://ngn.tf)
|
This repo contains all the source code for my personal website, [ngn.tf](https://ngn.tf)
|
||||||
All code is licensed under AGPL version 3 (see [LICENSE.txt](LICENSE.txt))
|
All code is licensed under AGPL version 3 (see [LICENSE.txt](LICENSE.txt))
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> I do not accept PRs as this is just my personal project.
|
|
||||||
|
|
||||||
## Directory structure
|
## Directory structure
|
||||||
### `app`
|
### `app`
|
||||||
Contains frontend application, written with SvelteKit. It supports full SSR.
|
Contains frontend application, written with SvelteKit. It supports full SSR.
|
||||||
@ -16,21 +13,21 @@ Contains modified CSS from [github-markdown-css](https://github.com/sindresorhus
|
|||||||
and fonts from [NerdFonts](https://www.nerdfonts.com/)
|
and fonts from [NerdFonts](https://www.nerdfonts.com/)
|
||||||
|
|
||||||
### `api`
|
### `api`
|
||||||
Contains the API server, written in Go. It uses the [Fiber](https://github.com/gofiber/fiber) web
|
Contains the API server, written in Go. It uses the [Fiber](https://github.com/gofiber/fiber) web
|
||||||
framework which offers an [Express](https://expressjs.com/) like experience. I choose Fiber since I've used worked with express a lot in the past. However previously the I was using
|
framework which offers an [Express](https://expressjs.com/) like experience. I choose Fiber since I've used worked with express a lot in the past. However previously the I was using
|
||||||
[Gin](https://github.com/gin-gonic/gin) (see history section).
|
[Gin](https://github.com/gin-gonic/gin) (see history section).
|
||||||
|
|
||||||
API stores all the data in a local sqlite(3) database. Go doesn't support sqlite3 out of the box so
|
API stores all the data in a local sqlite(3) database. Go doesn't support sqlite3 out of the box so
|
||||||
I'm using [mattn's sqlite3 driver](https://github.com/mattn/go-sqlite3).
|
I'm using [mattn's sqlite3 driver](https://github.com/mattn/go-sqlite3).
|
||||||
|
|
||||||
### `admin`
|
### `admin`
|
||||||
The frontend application does not contain an admin interface, I do the administration stuff (such as
|
The frontend application does not contain an admin interface, I do the administration stuff (such as
|
||||||
adding posts, adding services etc.) using the python script in this directory. This script can be
|
adding posts, adding services etc.) using the python script in this directory. This script can be
|
||||||
installed on to the PATH by running the `install.sh` script. After installation it can be used
|
installed on to the PATH by running the `install.sh` script. After installation it can be used
|
||||||
by running `admin_script`.
|
by running `admin_script`.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
Easiest way to deploy is to use docker. I have created a `compose.yml` file so the API and the
|
Easiest way to deploy is to use docker. I have created a `compose.yml` file so the API and the
|
||||||
frontend application can be deployed easily with just the `docker-compose up` command:
|
frontend application can be deployed easily with just the `docker-compose up` command:
|
||||||
```yaml
|
```yaml
|
||||||
version: "3"
|
version: "3"
|
||||||
@ -38,8 +35,8 @@ services:
|
|||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: ./app
|
context: ./app
|
||||||
args:
|
environment:
|
||||||
API_URL: "https://api.ngn.tf"
|
- API_URL: "https://api.ngn.tf"
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:7002:3000"
|
- "127.0.0.1:7002:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -48,8 +45,9 @@ services:
|
|||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
context: ./api
|
context: ./api
|
||||||
args:
|
environment:
|
||||||
PASSWORD: "securepassword"
|
- API_PASSWORD: "securepassword"
|
||||||
|
- API_FRONTEND_URL: "https://ngn.tf"
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:7001:7001"
|
- "127.0.0.1:7001:7001"
|
||||||
volumes:
|
volumes:
|
||||||
@ -59,34 +57,38 @@ services:
|
|||||||
## History
|
## History
|
||||||
Some nostalgic history/changelog stuff (just for the major version numbers):
|
Some nostalgic history/changelog stuff (just for the major version numbers):
|
||||||
|
|
||||||
- **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was just a simple HTML/CSS page,
|
- **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was just a simple HTML/CSS page,
|
||||||
I never published any of the source code and I wiped the local copy on my USB drive in early 2022.
|
I never published any of the source code and I wiped the local copy on my USB drive in early 2022
|
||||||
|
|
||||||
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png).
|
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code
|
||||||
|
was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png)
|
||||||
|
|
||||||
- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework)
|
- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework)
|
||||||
my website with one of the fancy JS frameworks. I decided to go with Svelte. Not the kit version,
|
my website with one of the fancy JS frameworks. I decided to go with Svelte. Not the kit version,
|
||||||
at the time svelte did not support SSR. I do not remember writting an API for it so I guess I just
|
at the time svelte did not support SSR. I do not remember writting an API for it so I guess I just
|
||||||
updated it everytime I wanted to add content? It was pretty much like a static website and was hosted
|
updated it everytime I wanted to add content? It was pretty much like a static website and was hosted
|
||||||
on `ngn13.fun` as at this point I had my own hosting. The source code for this website was in a
|
on `ngn13.fun` as at this point I had my own hosting. The source code for this website was in a
|
||||||
deleted github repository of mine, I looked for a local copy on my old hard drive but I wasn't able
|
deleted github repository of mine, I looked for a local copy on my old hard drive but I wasn't able
|
||||||
to find it. I also do not remember how it looked like, sooo this version is pretty much lost :(
|
to find it. I also do not remember how it looked like, sooo this version is pretty much lost :(
|
||||||
|
|
||||||
- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework
|
- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework
|
||||||
my website one more time in NuxtJS. I had really "fun time" using vue stuff. As NuxtJS supported
|
my website one more time in NuxtJS. I had really "fun time" using vue stuff. As NuxtJS supported
|
||||||
server-side code, this website had its own built in API. This website was also hosted on `ngn13.fun`
|
server-side code, this website had its own built in API. This website was also hosted on `ngn13.fun`
|
||||||
|
|
||||||
- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite and rework my website
|
- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite and rework my website
|
||||||
again, this time I was going with SvelteKit as I haven't had the greatest experience with NuxtJS.
|
again, this time I was going with SvelteKit as I haven't had the greatest experience with NuxtJS.
|
||||||
SvelteKit was really fun to work with and I got my new website done pretty quickly. (I don't wanna
|
SvelteKit was really fun to work with and I got my new website done pretty quickly. (I don't wanna
|
||||||
brag or something but I really imporeved the CSS/styling stuff ya know). I also wrote a new API
|
brag or something but I really imporeved the CSS/styling stuff ya know). I also wrote a new API
|
||||||
with Go and Gin. I did not publish the source code for the API, its still on my local gitea
|
with Go and Gin. I did not publish the source code for the API, its still on my local gitea
|
||||||
server tho. This website was hosted on `ngn13.fun` as well.
|
server tho. This website was hosted on `ngn13.fun` as well
|
||||||
|
|
||||||
- **v4.0 (2023 november -)** The current major version of my website. The frontend is still
|
- **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0,
|
||||||
similar to 3.0, the big changes are in the API. I rewrote the API with Fiber. This version is
|
the big changes are in the API. I rewrote the API with Fiber. This version was the first version which is hosted on
|
||||||
hosted on `ngn.tf` which is my new domain name btw.
|
`ngn.tf` which is my new domain name btw
|
||||||
|
|
||||||
## Screenshots
|
- **v5.0 (2024 october - ...)**: The current major version of my website, has small UI and API tweaks when
|
||||||
|
compared to 4.0
|
||||||
|
|
||||||
|
## Screenshots (from v4.0)
|
||||||
![](assets/4.0_index.png)
|
![](assets/4.0_index.png)
|
||||||
![](assets/4.0_blog.png)
|
![](assets/4.0_blog.png)
|
||||||
|
@ -7,17 +7,11 @@ COPY *.mod ./
|
|||||||
COPY *.sum ./
|
COPY *.sum ./
|
||||||
COPY Makefile ./
|
COPY Makefile ./
|
||||||
COPY routes ./routes
|
COPY routes ./routes
|
||||||
COPY global ./global
|
COPY global ./config
|
||||||
COPY database ./database
|
COPY database ./database
|
||||||
COPY util ./util
|
COPY util ./util
|
||||||
|
|
||||||
EXPOSE 7001
|
EXPOSE 7001
|
||||||
RUN make
|
RUN make
|
||||||
|
|
||||||
ARG PASSWORD
|
|
||||||
ENV PASSWORD $PASSWORD
|
|
||||||
|
|
||||||
ARG FRONTEND_URL
|
|
||||||
ENV FRONTEND_URL $FRONTEND_URL
|
|
||||||
|
|
||||||
ENTRYPOINT ["/app/server"]
|
ENTRYPOINT ["/app/server"]
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
all: server
|
all: server
|
||||||
|
|
||||||
server: *.go routes/*.go util/*.go global/*.go database/*.go
|
server: *.go routes/*.go database/*.go util/*.go config/*.go
|
||||||
go build -o $@ .
|
go build -o $@ .
|
||||||
|
|
||||||
test:
|
test:
|
||||||
FRONTEND_URL=http://localhost:5173/ PASSWORD=test ./server
|
API_FRONTEND_URL=http://localhost:5173/ API_PASSWORD=test ./server
|
||||||
|
|
||||||
format:
|
format:
|
||||||
gofmt -s -w .
|
gofmt -s -w .
|
||||||
|
60
api/config/config.go
Normal file
60
api/config/config.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Required bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Option) Env() string {
|
||||||
|
return strings.ToUpper(fmt.Sprintf("API_%s", o.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
var options []Option = []Option{
|
||||||
|
{Name: "password", Value: "", Required: true},
|
||||||
|
{Name: "frontend_url", Value: "http://localhost:5173/", Required: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() bool {
|
||||||
|
var val string
|
||||||
|
|
||||||
|
for i := range options {
|
||||||
|
if val = os.Getenv(options[i].Env()); val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
options[i].Value = val
|
||||||
|
options[i].Required = false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range options {
|
||||||
|
if options[i].Required && options[i].Value == "" {
|
||||||
|
util.Fail("please specify the required config option \"%s\" (\"%s\")", options[i].Name, options[i].Env())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if options[i].Required && options[i].Value != "" {
|
||||||
|
util.Fail("using the default value \"%s\" for required config option \"%s\" (\"%s\")", options[i].Value, options[i].Name, options[i].Env())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(name string) string {
|
||||||
|
for i := range options {
|
||||||
|
if options[i].Name != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return options[i].Value
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
@ -2,18 +2,10 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"github.com/ngn13/website/api/global"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Type struct {
|
func Setup(db *sql.DB) error {
|
||||||
Sql *sql.DB
|
_, err := db.Exec(`
|
||||||
Votes []global.Vote
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Type) Setup() error {
|
|
||||||
t.Votes = []global.Vote{}
|
|
||||||
|
|
||||||
_, err := t.Sql.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS posts(
|
CREATE TABLE IF NOT EXISTS posts(
|
||||||
id TEXT NOT NULL UNIQUE,
|
id TEXT NOT NULL UNIQUE,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
@ -29,7 +21,7 @@ func (t *Type) Setup() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = t.Sql.Exec(`
|
_, err = db.Exec(`
|
||||||
CREATE TABLE IF NOT EXISTS services(
|
CREATE TABLE IF NOT EXISTS services(
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
desc TEXT NOT NULL,
|
desc TEXT NOT NULL,
|
||||||
@ -37,19 +29,16 @@ func (t *Type) Setup() error {
|
|||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
|
|
||||||
return err
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Type) Open(p string) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if t.Sql, err = sql.Open("sqlite3", p); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.Setup()
|
_, err = db.Exec(`
|
||||||
}
|
CREATE TABLE IF NOT EXISTS votes(
|
||||||
|
hash TEXT NOT NULL UNIQUE,
|
||||||
|
is_upvote INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
func (t *Type) Close() {
|
return err
|
||||||
t.Sql.Close()
|
|
||||||
}
|
}
|
||||||
|
71
api/database/post.go
Normal file
71
api/database/post.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Public int `json:"public"`
|
||||||
|
Vote int `json:"vote"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Post) Load(rows *sql.Rows) error {
|
||||||
|
return rows.Scan(&p.ID, &p.Title, &p.Author, &p.Date, &p.Content, &p.Public, &p.Vote)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Post) Get(db *sql.DB, id string) (bool, error) {
|
||||||
|
var (
|
||||||
|
success bool
|
||||||
|
rows *sql.Rows
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if rows, err = db.Query("SELECT * FROM posts WHERE id = ?", id); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if success = rows.Next(); !success {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.Load(rows); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Post) Remove(db *sql.DB) error {
|
||||||
|
_, err := db.Exec("DELETE FROM posts WHERE id = ?", p.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Post) Save(db *sql.DB) error {
|
||||||
|
p.ID = util.TitleToID(p.Title)
|
||||||
|
|
||||||
|
_, err := db.Exec(
|
||||||
|
"INSERT INTO posts(id, title, author, date, content, public, vote) values(?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
p.ID, p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Post) Update(db *sql.DB) error {
|
||||||
|
p.ID = util.TitleToID(p.Title)
|
||||||
|
|
||||||
|
_, err := db.Exec(
|
||||||
|
"UPDATE posts SET title = ?, author = ?, date = ?, content = ?, public = ?, vote = ? WHERE id = ?",
|
||||||
|
p.Title, p.Author, p.Date, p.Content, p.Public, p.Vote, p.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
56
api/database/service.go
Normal file
56
api/database/service.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Load(rows *sql.Rows) error {
|
||||||
|
return rows.Scan(&s.Name, &s.Desc, &s.Url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(db *sql.DB, name string) (bool, error) {
|
||||||
|
var (
|
||||||
|
success bool
|
||||||
|
rows *sql.Rows
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if rows, err = db.Query("SELECT * FROM services WHERE name = ?", name); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if success = rows.Next(); !success {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.Load(rows); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Remove(db *sql.DB) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"DELETE FROM services WHERE name = ?",
|
||||||
|
s.Name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Save(db *sql.DB) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"INSERT INTO services(name, desc, url) values(?, ?, ?)",
|
||||||
|
s.Name, s.Desc, s.Url,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
49
api/database/vote.go
Normal file
49
api/database/vote.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
|
||||||
|
type Vote struct {
|
||||||
|
Hash string
|
||||||
|
IsUpvote bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vote) Load(rows *sql.Rows) error {
|
||||||
|
return rows.Scan(&v.Hash, &v.IsUpvote)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vote) Get(db *sql.DB, hash string) (bool, error) {
|
||||||
|
var (
|
||||||
|
success bool
|
||||||
|
rows *sql.Rows
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if rows, err = db.Query("SELECT * FROM votes WHERE hash = ?", hash); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if success = rows.Next(); !success {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = v.Load(rows); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vote) Update(db *sql.DB) error {
|
||||||
|
_, err := db.Exec("UPDATE votes SET is_upvote = ? WHERE hash = ?", v.IsUpvote, v.Hash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vote) Save(db *sql.DB) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"INSERT INTO votes(hash, is_upvote) values(?, ?)",
|
||||||
|
v.Hash, v.IsUpvote,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
74
api/main.go
74
api/main.go
@ -1,27 +1,40 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"database/sql"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
"github.com/ngn13/website/api/routes"
|
"github.com/ngn13/website/api/routes"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var db *sql.DB
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
app *fiber.App
|
app *fiber.App
|
||||||
db database.Type
|
//db *sql.DB
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if err = db.Open("data.db"); err != nil {
|
if !config.Load() {
|
||||||
log.Fatalf("Cannot access the database: %s", err.Error())
|
util.Fail("failed to load the configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if db, err = sql.Open("sqlite3", "data.db"); err != nil {
|
||||||
|
util.Fail("cannot access the database: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
|
if err = database.Setup(db); err != nil {
|
||||||
|
util.Fail("cannot setup the database: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
app = fiber.New(fiber.Config{
|
app = fiber.New(fiber.Config{
|
||||||
DisableStartupMessage: true,
|
DisableStartupMessage: true,
|
||||||
})
|
})
|
||||||
@ -31,7 +44,9 @@ func main() {
|
|||||||
c.Set("Access-Control-Allow-Headers",
|
c.Set("Access-Control-Allow-Headers",
|
||||||
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||||
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET")
|
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET")
|
||||||
|
|
||||||
c.Locals("database", &db)
|
c.Locals("database", &db)
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -42,37 +57,46 @@ func main() {
|
|||||||
|
|
||||||
// blog routes
|
// blog routes
|
||||||
blog_routes := app.Group("/blog")
|
blog_routes := app.Group("/blog")
|
||||||
blog_routes.Get("/feed.atom", routes.GetAtomFeed)
|
|
||||||
blog_routes.Get("/feed.rss", routes.GetRSSFeed)
|
// blog feed routes
|
||||||
blog_routes.Get("/feed.json", routes.GetJSONFeed)
|
blog_routes.Get("/feed.*", routes.GET_Feed)
|
||||||
blog_routes.Get("/sum", routes.SumPost)
|
|
||||||
blog_routes.Get("/get", routes.GetPost)
|
// blog post routes
|
||||||
blog_routes.Get("/vote/set", routes.VoteSet)
|
blog_routes.Get("/sum", routes.GET_PostSum)
|
||||||
blog_routes.Get("/vote/status", routes.VoteStat)
|
blog_routes.Get("/get", routes.GET_Post)
|
||||||
|
|
||||||
|
// blog post vote routes
|
||||||
|
blog_routes.Get("/vote/set", routes.GET_VoteSet)
|
||||||
|
blog_routes.Get("/vote/get", routes.GET_VoteGet)
|
||||||
|
|
||||||
// service routes
|
// service routes
|
||||||
service_routes := app.Group("services")
|
service_routes := app.Group("services")
|
||||||
service_routes.Get("/all", routes.GetServices)
|
service_routes.Get("/all", routes.GET_Services)
|
||||||
|
|
||||||
// admin routes
|
// admin routes
|
||||||
admin_routes := app.Group("admin")
|
admin_routes := app.Group("admin")
|
||||||
admin_routes.Use("*", routes.AuthMiddleware)
|
admin_routes.Use("*", routes.AuthMiddleware)
|
||||||
admin_routes.Get("/login", routes.Login)
|
|
||||||
admin_routes.Get("/logout", routes.Logout)
|
|
||||||
admin_routes.Put("/service/add", routes.AddService)
|
|
||||||
admin_routes.Delete("/service/remove", routes.RemoveService)
|
|
||||||
admin_routes.Put("/blog/add", routes.AddPost)
|
|
||||||
admin_routes.Delete("/blog/remove", routes.RemovePost)
|
|
||||||
|
|
||||||
// 404 routes
|
// admin auth routes
|
||||||
|
admin_routes.Get("/login", routes.GET_Login)
|
||||||
|
admin_routes.Get("/logout", routes.GET_Logout)
|
||||||
|
|
||||||
|
// admin service managment routes
|
||||||
|
admin_routes.Put("/service/add", routes.PUT_AddService)
|
||||||
|
admin_routes.Delete("/service/remove", routes.DEL_RemoveService)
|
||||||
|
|
||||||
|
// admin blog managment routes
|
||||||
|
admin_routes.Put("/blog/add", routes.PUT_AddPost)
|
||||||
|
admin_routes.Delete("/blog/remove", routes.DEL_RemovePost)
|
||||||
|
|
||||||
|
// 404 route
|
||||||
app.All("*", func(c *fiber.Ctx) error {
|
app.All("*", func(c *fiber.Ctx) error {
|
||||||
return c.Status(http.StatusNotFound).JSON(fiber.Map{
|
return util.ErrNotFound(c)
|
||||||
"error": "Requested endpoint not found",
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Println("Starting web server at port 7001")
|
util.Info("starting web server at port 7001")
|
||||||
|
|
||||||
if err = app.Listen("0.0.0.0:7001"); err != nil {
|
if err = app.Listen("0.0.0.0:7001"); err != nil {
|
||||||
log.Printf("Error starting the webserver: %s", err.Error())
|
util.Fail("error starting the webserver: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"database/sql"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/mattn/go-sqlite3"
|
"github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
"github.com/ngn13/website/api/global"
|
|
||||||
"github.com/ngn13/website/api/util"
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,14 +27,12 @@ func AuthMiddleware(c *fiber.Ctx) error {
|
|||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Login(c *fiber.Ctx) error {
|
func GET_Login(c *fiber.Ctx) error {
|
||||||
if c.Query("pass") != os.Getenv("PASSWORD") {
|
if c.Query("pass") != config.Get("password") {
|
||||||
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{
|
return util.ErrAuth(c)
|
||||||
"error": "Authentication failed",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("New login from %s", util.GetIP(c))
|
util.Info("new login from %s", util.GetIP(c))
|
||||||
|
|
||||||
return c.Status(http.StatusOK).JSON(fiber.Map{
|
return c.Status(http.StatusOK).JSON(fiber.Map{
|
||||||
"error": "",
|
"error": "",
|
||||||
@ -43,44 +40,58 @@ func Login(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Logout(c *fiber.Ctx) error {
|
func GET_Logout(c *fiber.Ctx) error {
|
||||||
Token = util.CreateToken()
|
Token = util.CreateToken()
|
||||||
|
|
||||||
log.Printf("Logout from %s", util.GetIP(c))
|
util.Info("logout from %s", util.GetIP(c))
|
||||||
|
|
||||||
return c.Status(http.StatusOK).JSON(fiber.Map{
|
return c.Status(http.StatusOK).JSON(fiber.Map{
|
||||||
"error": "",
|
"error": "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveService(c *fiber.Ctx) error {
|
func DEL_RemoveService(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
db *database.Type
|
db *sql.DB
|
||||||
name string
|
service database.Service
|
||||||
|
name string
|
||||||
|
found bool
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
db = c.Locals("database").(*database.Type)
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
name = c.Query("name")
|
name = c.Query("name")
|
||||||
|
|
||||||
if name == "" {
|
if name == "" {
|
||||||
util.ErrBadData(c)
|
util.ErrBadData(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Sql.Exec("DELETE FROM services WHERE name = ?", name)
|
if found, err = service.Get(db, name); err != nil {
|
||||||
if util.ErrorCheck(err, c) {
|
util.Fail("error while searching for a service (\"%s\"): %s", name, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return util.ErrEntryNotExists(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = service.Remove(db); err != nil {
|
||||||
|
util.Fail("error while removing a service (\"%s\"): %s", service.Name, err.Error())
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.NoError(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddService(c *fiber.Ctx) error {
|
func PUT_AddService(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
service global.Service
|
service database.Service
|
||||||
db *database.Type
|
db *sql.DB
|
||||||
|
found bool
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
db = c.Locals("database").(*database.Type)
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
|
|
||||||
if c.BodyParser(&service) != nil {
|
if c.BodyParser(&service) != nil {
|
||||||
return util.ErrBadJSON(c)
|
return util.ErrBadJSON(c)
|
||||||
@ -90,58 +101,63 @@ func AddService(c *fiber.Ctx) error {
|
|||||||
return util.ErrBadData(c)
|
return util.ErrBadData(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := db.Sql.Query("SELECT * FROM services WHERE name = ?", service.Name)
|
if found, err = service.Get(db, service.Name); err != nil {
|
||||||
if util.ErrorCheck(err, c) {
|
util.Fail("error while searching for a service (\"%s\"): %s", service.Name, err.Error())
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rows.Next() {
|
if found {
|
||||||
rows.Close()
|
return util.ErrEntryExists(c)
|
||||||
return util.ErrExists(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.Close()
|
if err = service.Save(db); err != nil {
|
||||||
|
util.Fail("error while saving a new service (\"%s\"): %s", service.Name, err.Error())
|
||||||
_, err = db.Sql.Exec(
|
|
||||||
"INSERT INTO services(name, desc, url) values(?, ?, ?)",
|
|
||||||
service.Name, service.Desc, service.Url,
|
|
||||||
)
|
|
||||||
|
|
||||||
if util.ErrorCheck(err, c) {
|
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.NoError(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemovePost(c *fiber.Ctx) error {
|
func DEL_RemovePost(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
db *database.Type
|
db *sql.DB
|
||||||
id string
|
id string
|
||||||
|
found bool
|
||||||
|
err error
|
||||||
|
post database.Post
|
||||||
)
|
)
|
||||||
|
|
||||||
db = c.Locals("database").(*database.Type)
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
id = c.Query("id")
|
|
||||||
|
|
||||||
if id == "" {
|
if id = c.Query("id"); id == "" {
|
||||||
return util.ErrBadData(c)
|
return util.ErrBadData(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Sql.Exec("DELETE FROM posts WHERE id = ?", id)
|
if found, err = post.Get(db, id); err != nil {
|
||||||
if util.ErrorCheck(err, c) {
|
util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return util.ErrEntryNotExists(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = post.Remove(db); err != nil {
|
||||||
|
util.Fail("error while removing a post (\"%s\"): %s", post.ID, err.Error())
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.NoError(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddPost(c *fiber.Ctx) error {
|
func PUT_AddPost(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
db *database.Type
|
db *sql.DB
|
||||||
post global.Post
|
post database.Post
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
db = c.Locals("database").(*database.Type)
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
post.Public = 1
|
post.Public = 1
|
||||||
|
|
||||||
if c.BodyParser(&post) != nil {
|
if c.BodyParser(&post) != nil {
|
||||||
@ -153,19 +169,14 @@ func AddPost(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post.Date = time.Now().Format("02/01/06")
|
post.Date = time.Now().Format("02/01/06")
|
||||||
post.ID = util.TitleToID(post.Title)
|
|
||||||
|
|
||||||
_, err := db.Sql.Exec(
|
if err = post.Save(db); err != nil && strings.Contains(err.Error(), sqlite3.ErrConstraintUnique.Error()) {
|
||||||
"INSERT INTO posts(id, title, author, date, content, public, vote) values(?, ?, ?, ?, ?, ?, ?)",
|
return util.ErrEntryExists(c)
|
||||||
post.ID, post.Title, post.Author, post.Date, post.Content, post.Public, post.Vote,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil && strings.Contains(err.Error(), sqlite3.ErrConstraintUnique.Error()) {
|
|
||||||
return util.ErrExists(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if util.ErrorCheck(err, c) {
|
if err != nil {
|
||||||
return util.ErrExists(c)
|
util.Fail("error while saving a new post (\"%s\"): %s", post.ID, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.NoError(c)
|
return util.NoError(c)
|
||||||
|
@ -3,164 +3,40 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"path"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gorilla/feeds"
|
"github.com/gorilla/feeds"
|
||||||
|
"github.com/ngn13/website/api/config"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
"github.com/ngn13/website/api/global"
|
|
||||||
"github.com/ngn13/website/api/util"
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PostFromRow(post *global.Post, rows *sql.Rows) error {
|
func GET_Post(c *fiber.Ctx) error {
|
||||||
err := rows.Scan(&post.ID, &post.Title, &post.Author, &post.Date, &post.Content, &post.Public, &post.Vote)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPostByID(db *database.Type, id string) (global.Post, string) {
|
|
||||||
var post global.Post = global.Post{}
|
|
||||||
post.Title = "NONE"
|
|
||||||
|
|
||||||
rows, err := db.Sql.Query("SELECT * FROM posts WHERE id = ?", id)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return post, "Server error"
|
|
||||||
}
|
|
||||||
|
|
||||||
success := rows.Next()
|
|
||||||
if !success {
|
|
||||||
rows.Close()
|
|
||||||
return post, "Post not found"
|
|
||||||
}
|
|
||||||
|
|
||||||
err = PostFromRow(&post, rows)
|
|
||||||
if err != nil {
|
|
||||||
rows.Close()
|
|
||||||
return post, "Server error"
|
|
||||||
}
|
|
||||||
rows.Close()
|
|
||||||
|
|
||||||
if post.Title == "NONE" {
|
|
||||||
return post, "Post not found"
|
|
||||||
}
|
|
||||||
|
|
||||||
return post, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func VoteStat(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
db *database.Type
|
|
||||||
id string
|
|
||||||
)
|
|
||||||
|
|
||||||
db = c.Locals("database").(*database.Type)
|
|
||||||
id = c.Query("id")
|
|
||||||
|
|
||||||
if id == "" {
|
|
||||||
return util.ErrBadData(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(db.Votes); i++ {
|
|
||||||
if db.Votes[i].Client == util.GetIP(c) && db.Votes[i].Post == id {
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"error": "",
|
|
||||||
"result": db.Votes[i].Status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(http.StatusNotFound).JSON(util.ErrorJSON("Client never voted"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func VoteSet(c *fiber.Ctx) error {
|
|
||||||
var (
|
var (
|
||||||
|
post database.Post
|
||||||
id string
|
id string
|
||||||
to string
|
db *sql.DB
|
||||||
voted bool
|
found bool
|
||||||
db *database.Type
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
db = c.Locals("database").(*database.Type)
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
id = c.Query("id")
|
|
||||||
to = c.Query("to")
|
|
||||||
voted = false
|
|
||||||
|
|
||||||
if id == "" || (to != "upvote" && to != "downvote") {
|
if id = c.Query("id"); id == "" {
|
||||||
return util.ErrBadData(c)
|
return util.ErrBadData(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(db.Votes); i++ {
|
if found, err = post.Get(db, id); err != nil {
|
||||||
if db.Votes[i].Client == util.GetIP(c) && db.Votes[i].Post == id && db.Votes[i].Status == to {
|
util.Fail("error while search for a post (\"%s\"): %s", id, err.Error())
|
||||||
return c.Status(http.StatusForbidden).JSON(util.ErrorJSON("Client already voted"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if db.Votes[i].Client == util.GetIP(c) && db.Votes[i].Post == id && db.Votes[i].Status != to {
|
|
||||||
voted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post, msg := GetPostByID(db, id)
|
|
||||||
if msg != "" {
|
|
||||||
return c.Status(http.StatusNotFound).JSON(util.ErrorJSON(msg))
|
|
||||||
}
|
|
||||||
|
|
||||||
vote := post.Vote + 1
|
|
||||||
|
|
||||||
if to == "downvote" {
|
|
||||||
vote = post.Vote - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if to == "downvote" && voted {
|
|
||||||
vote = vote - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if to == "upvote" && voted {
|
|
||||||
vote = vote + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := db.Sql.Exec("UPDATE posts SET vote = ? WHERE title = ?", vote, post.Title)
|
|
||||||
if util.ErrorCheck(err, c) {
|
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(db.Votes); i++ {
|
if !found {
|
||||||
if db.Votes[i].Client == util.GetIP(c) && db.Votes[i].Post == id && db.Votes[i].Status != to {
|
return util.ErrEntryNotExists(c)
|
||||||
db.Votes[i].Status = to
|
|
||||||
return util.NoError(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var entry = global.Vote{}
|
|
||||||
entry.Client = util.GetIP(c)
|
|
||||||
entry.Status = to
|
|
||||||
entry.Post = id
|
|
||||||
db.Votes = append(db.Votes, entry)
|
|
||||||
return util.NoError(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPost(c *fiber.Ctx) error {
|
|
||||||
var (
|
|
||||||
id string
|
|
||||||
db *database.Type
|
|
||||||
)
|
|
||||||
|
|
||||||
id = c.Query("id")
|
|
||||||
db = c.Locals("database").(*database.Type)
|
|
||||||
|
|
||||||
if id == "" {
|
|
||||||
return util.ErrBadData(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
post, msg := GetPostByID(db, id)
|
|
||||||
if msg != "" {
|
|
||||||
return c.Status(http.StatusNotFound).JSON(util.ErrorJSON(msg))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
@ -169,25 +45,27 @@ func GetPost(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func SumPost(c *fiber.Ctx) error {
|
func GET_PostSum(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
posts []global.Post
|
posts []database.Post
|
||||||
post global.Post
|
rows *sql.Rows
|
||||||
db *database.Type
|
db *sql.DB
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
db = c.Locals("database").(*database.Type)
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
|
|
||||||
rows, err := db.Sql.Query("SELECT * FROM posts")
|
if rows, err = db.Query("SELECT * FROM posts"); err != nil {
|
||||||
if util.ErrorCheck(err, c) {
|
util.Fail("cannot load posts: %s", err.Error())
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err = PostFromRow(&post, rows)
|
var post database.Post
|
||||||
|
|
||||||
if util.ErrorCheck(err, c) {
|
if err = post.Load(rows); err != nil {
|
||||||
|
util.Fail("error while loading post: %s", err.Error())
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +79,6 @@ func SumPost(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
posts = append(posts, post)
|
posts = append(posts, post)
|
||||||
}
|
}
|
||||||
rows.Close()
|
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"error": "",
|
"error": "",
|
||||||
@ -209,22 +86,21 @@ func SumPost(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFeed(db *database.Type) (*feeds.Feed, error) {
|
func getFeed(db *sql.DB) (*feeds.Feed, error) {
|
||||||
var (
|
var (
|
||||||
posts []global.Post
|
posts []database.Post
|
||||||
post global.Post
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
rows, err := db.Sql.Query("SELECT * FROM posts")
|
rows, err := db.Query("SELECT * FROM posts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err := PostFromRow(&post, rows)
|
var post database.Post
|
||||||
|
|
||||||
if err != nil {
|
if err = post.Load(rows); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +112,10 @@ func GetFeed(db *database.Type) (*feeds.Feed, error) {
|
|||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
|
|
||||||
blogurl, err := url.JoinPath(os.Getenv("FRONTEND_URL"), "/blog")
|
blogurl, err := url.JoinPath(
|
||||||
|
config.Get("frontend_url"), "/blog",
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create the blog URL: %s", err.Error())
|
return nil, fmt.Errorf("failed to create the blog URL: %s", err.Error())
|
||||||
}
|
}
|
||||||
@ -273,46 +152,52 @@ func GetFeed(db *database.Type) (*feeds.Feed, error) {
|
|||||||
return feed, nil
|
return feed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAtomFeed(c *fiber.Ctx) error {
|
func GET_Feed(c *fiber.Ctx) error {
|
||||||
feed, err := GetFeed(c.Locals("database").(*database.Type))
|
var (
|
||||||
if util.ErrorCheck(err, c) {
|
db *sql.DB
|
||||||
|
err error
|
||||||
|
feed *feeds.Feed
|
||||||
|
name []string
|
||||||
|
res string
|
||||||
|
ext string
|
||||||
|
)
|
||||||
|
|
||||||
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
|
|
||||||
|
if name = strings.Split(path.Base(c.Path()), "."); len(name) != 2 {
|
||||||
|
return util.ErrNotFound(c)
|
||||||
|
}
|
||||||
|
ext = name[1]
|
||||||
|
|
||||||
|
if feed, err = getFeed(db); err != nil {
|
||||||
|
util.Fail("cannot obtain the feed: %s", err.Error())
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
atom, err := feed.ToAtom()
|
switch ext {
|
||||||
if util.ErrorCheck(err, c) {
|
case "atom":
|
||||||
|
res, err = feed.ToAtom()
|
||||||
|
c.Set("Content-Type", "application/atom+xml")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "json":
|
||||||
|
res, err = feed.ToJSON()
|
||||||
|
c.Set("Content-Type", "application/feed+json")
|
||||||
|
break
|
||||||
|
|
||||||
|
case "rss":
|
||||||
|
res, err = feed.ToRss()
|
||||||
|
c.Set("Content-Type", "application/rss+xml")
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
return util.ErrNotFound(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.Fail("cannot obtain the feed as the specified format: %s", err.Error())
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Set("Content-Type", "application/atom+xml")
|
return c.Send([]byte(res))
|
||||||
return c.Send([]byte(atom))
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRSSFeed(c *fiber.Ctx) error {
|
|
||||||
feed, err := GetFeed(c.Locals("database").(*database.Type))
|
|
||||||
if util.ErrorCheck(err, c) {
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
rss, err := feed.ToRss()
|
|
||||||
if util.ErrorCheck(err, c) {
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set("Content-Type", "application/rss+xml")
|
|
||||||
return c.Send([]byte(rss))
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetJSONFeed(c *fiber.Ctx) error {
|
|
||||||
feed, err := GetFeed(c.Locals("database").(*database.Type))
|
|
||||||
if util.ErrorCheck(err, c) {
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
json, err := feed.ToJSON()
|
|
||||||
if util.ErrorCheck(err, c) {
|
|
||||||
return util.ErrServer(c)
|
|
||||||
}
|
|
||||||
c.Set("Content-Type", "application/feed+json")
|
|
||||||
return c.Send([]byte(json))
|
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,40 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"database/sql"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/ngn13/website/api/database"
|
"github.com/ngn13/website/api/database"
|
||||||
"github.com/ngn13/website/api/global"
|
|
||||||
"github.com/ngn13/website/api/util"
|
"github.com/ngn13/website/api/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetServices(c *fiber.Ctx) error {
|
func GET_Services(c *fiber.Ctx) error {
|
||||||
var (
|
var (
|
||||||
services []global.Service = []global.Service{}
|
services []database.Service
|
||||||
service global.Service
|
rows *sql.Rows
|
||||||
db *database.Type
|
db *sql.DB
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
db = c.Locals("database").(*database.Type)
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
|
|
||||||
rows, err := db.Sql.Query("SELECT * FROM services")
|
if rows, err = db.Query("SELECT * FROM services"); err != nil {
|
||||||
if util.ErrorCheck(err, c) {
|
util.Fail("cannot load services: %s", err.Error())
|
||||||
return util.ErrServer(c)
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
if err = rows.Scan(&service.Name, &service.Desc, &service.Url); err != nil {
|
var service database.Service
|
||||||
log.Println("Error scaning services row: " + err.Error())
|
|
||||||
continue
|
if err = service.Load(rows); err != nil {
|
||||||
|
util.Fail("error while loading service: %s", err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
services = append(services, service)
|
services = append(services, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.Close()
|
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"error": "",
|
"error": "",
|
||||||
"result": services,
|
"result": services,
|
||||||
|
139
api/routes/vote.go
Normal file
139
api/routes/vote.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/ngn13/website/api/database"
|
||||||
|
"github.com/ngn13/website/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getVoteHash(id string, ip string) string {
|
||||||
|
return util.GetSHA512(id + "_" + ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET_VoteGet(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
db *sql.DB
|
||||||
|
id string
|
||||||
|
hash string
|
||||||
|
vote database.Vote
|
||||||
|
found bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
|
|
||||||
|
if id = c.Query("id"); id == "" {
|
||||||
|
return util.ErrBadData(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = getVoteHash(id, util.GetIP(c))
|
||||||
|
|
||||||
|
if found, err = vote.Get(db, hash); err != nil {
|
||||||
|
util.Fail("error while searchig for a vote (\"%s\"): %s", hash, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return util.ErrEntryNotExists(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vote.IsUpvote {
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"error": "",
|
||||||
|
"result": "upvote",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"error": "",
|
||||||
|
"result": "downvote",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GET_VoteSet(c *fiber.Ctx) error {
|
||||||
|
var (
|
||||||
|
db *sql.DB
|
||||||
|
id string
|
||||||
|
is_upvote bool
|
||||||
|
hash string
|
||||||
|
vote database.Vote
|
||||||
|
post database.Post
|
||||||
|
found bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
db = *(c.Locals("database").(**sql.DB))
|
||||||
|
id = c.Query("id")
|
||||||
|
|
||||||
|
if c.Query("to") == "" || id == "" {
|
||||||
|
return util.ErrBadData(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if found, err = post.Get(db, id); err != nil {
|
||||||
|
util.Fail("error while searching for a post (\"%s\"): %s", id, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return util.ErrEntryNotExists(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
is_upvote = c.Query("to") == "upvote"
|
||||||
|
hash = getVoteHash(id, util.GetIP(c))
|
||||||
|
|
||||||
|
if found, err = vote.Get(db, hash); err != nil {
|
||||||
|
util.Fail("error while searching for a vote (\"%s\"): %s", hash, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
if vote.IsUpvote == is_upvote {
|
||||||
|
return util.ErrEntryExists(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vote.IsUpvote && !is_upvote {
|
||||||
|
post.Vote -= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if !vote.IsUpvote && is_upvote {
|
||||||
|
post.Vote += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
vote.IsUpvote = is_upvote
|
||||||
|
|
||||||
|
if err = post.Update(db); err != nil {
|
||||||
|
util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = vote.Update(db); err != nil {
|
||||||
|
util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.NoError(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
vote.Hash = hash
|
||||||
|
vote.IsUpvote = is_upvote
|
||||||
|
|
||||||
|
if is_upvote {
|
||||||
|
post.Vote++
|
||||||
|
} else {
|
||||||
|
post.Vote--
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = post.Update(db); err != nil {
|
||||||
|
util.Fail("error while updating post (\"%s\"): %s", post.ID, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = vote.Save(db); err != nil {
|
||||||
|
util.Fail("error while updating vote (\"%s\"): %s", vote.Hash, err.Error())
|
||||||
|
return util.ErrServer(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.NoError(c)
|
||||||
|
}
|
12
api/util/log.go
Normal file
12
api/util/log.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Info = log.New(os.Stdout, "\033[34m[info]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
||||||
|
Warn = log.New(os.Stderr, "\033[33m[warn]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
||||||
|
Fail = log.New(os.Stderr, "\033[31m[fail]\033[0m ", log.Ltime|log.Lshortfile).Printf
|
||||||
|
)
|
@ -1,7 +1,8 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"crypto/sha512"
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -9,6 +10,11 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func GetSHA512(s string) string {
|
||||||
|
hasher := sha512.New()
|
||||||
|
return fmt.Sprintf("%x", hasher.Sum([]byte(s)))
|
||||||
|
}
|
||||||
|
|
||||||
func TitleToID(name string) string {
|
func TitleToID(name string) string {
|
||||||
return strings.ToLower(strings.ReplaceAll(name, " ", ""))
|
return strings.ToLower(strings.ReplaceAll(name, " ", ""))
|
||||||
}
|
}
|
||||||
@ -21,15 +27,6 @@ func CreateToken() string {
|
|||||||
return string(s)
|
return string(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrorCheck(err error, c *fiber.Ctx) bool {
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Server error: '%s' on %s\n", err, c.Path())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorJSON(error string) fiber.Map {
|
func ErrorJSON(error string) fiber.Map {
|
||||||
return fiber.Map{
|
return fiber.Map{
|
||||||
"error": error,
|
"error": error,
|
||||||
@ -48,10 +45,14 @@ func ErrServer(c *fiber.Ctx) error {
|
|||||||
return c.Status(http.StatusInternalServerError).JSON(ErrorJSON("Server error"))
|
return c.Status(http.StatusInternalServerError).JSON(ErrorJSON("Server error"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrExists(c *fiber.Ctx) error {
|
func ErrEntryExists(c *fiber.Ctx) error {
|
||||||
return c.Status(http.StatusConflict).JSON(ErrorJSON("Entry already exists"))
|
return c.Status(http.StatusConflict).JSON(ErrorJSON("Entry already exists"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrEntryNotExists(c *fiber.Ctx) error {
|
||||||
|
return c.Status(http.StatusNotFound).JSON(ErrorJSON("Entry does not exist"))
|
||||||
|
}
|
||||||
|
|
||||||
func ErrBadData(c *fiber.Ctx) error {
|
func ErrBadData(c *fiber.Ctx) error {
|
||||||
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Provided data is invalid"))
|
return c.Status(http.StatusBadRequest).JSON(ErrorJSON("Provided data is invalid"))
|
||||||
}
|
}
|
||||||
@ -64,6 +65,10 @@ func ErrAuth(c *fiber.Ctx) error {
|
|||||||
return c.Status(http.StatusUnauthorized).JSON(ErrorJSON("Authentication failed"))
|
return c.Status(http.StatusUnauthorized).JSON(ErrorJSON("Authentication failed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrNotFound(c *fiber.Ctx) error {
|
||||||
|
return c.Status(http.StatusNotFound).JSON(ErrorJSON("Requested endpoint not found"))
|
||||||
|
}
|
||||||
|
|
||||||
func NoError(c *fiber.Ctx) error {
|
func NoError(c *fiber.Ctx) error {
|
||||||
return c.Status(http.StatusOK).JSON(ErrorJSON(""))
|
return c.Status(http.StatusOK).JSON(ErrorJSON(""))
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
FROM node:22.8.0 as build
|
FROM node:22.8.0 as build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
ARG API_URL
|
ENV VITE_API_URL_DEV http://placeholder/
|
||||||
ENV VITE_API_URL_DEV $API_URL
|
|
||||||
|
|
||||||
RUN npm install && npm run build
|
RUN npm install && npm run build
|
||||||
|
|
||||||
@ -19,4 +18,5 @@ COPY --from=build /app/package-lock.json ./package-lock.json
|
|||||||
EXPOSE 4173
|
EXPOSE 4173
|
||||||
|
|
||||||
RUN bun install
|
RUN bun install
|
||||||
CMD ["bun", "build/index.js"]
|
|
||||||
|
CMD ["./docker/entry.sh"]
|
||||||
|
7
app/docker/entry.sh
Normal file
7
app/docker/entry.sh
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# replace the API URL
|
||||||
|
find ./build -type f -exec sed "s/http:\/\/placeholder\//${API_URL//\//\\/}/g" -i "{}" \;
|
||||||
|
|
||||||
|
# start the application
|
||||||
|
bun build/index.js
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "4.8.0",
|
"version": "5.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "VITE_API_URL_DEV=http://127.0.0.1:7001 vite dev",
|
"dev": "VITE_API_URL_DEV=http://127.0.0.1:7001 vite dev",
|
||||||
|
@ -30,11 +30,12 @@
|
|||||||
background: var(--dark-three);
|
background: var(--dark-three);
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
border: solid 1px var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
background: var(--dark-two);
|
background: var(--dark-two);
|
||||||
padding: 30px;
|
padding: 25px;
|
||||||
border-radius: 7px 7px 0px 0px;
|
border-radius: 7px 7px 0px 0px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
||||||
@ -43,8 +44,7 @@
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
background: var(--dark-three);
|
background: var(--dark-three);
|
||||||
padding: 40px;
|
padding: 30px;
|
||||||
padding-top: 30px;
|
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
|
@ -42,6 +42,7 @@ a {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: .4s;
|
transition: .4s;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
border: solid 1px var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover > .title {
|
a:hover > .title {
|
||||||
@ -51,7 +52,7 @@ a:hover > .title {
|
|||||||
.title {
|
.title {
|
||||||
border: solid 1px var(--dark-two);
|
border: solid 1px var(--dark-two);
|
||||||
background: var(--dark-two);
|
background: var(--dark-two);
|
||||||
padding: 30px;
|
padding: 25px;
|
||||||
border-radius: 7px 7px 0px 0px;
|
border-radius: 7px 7px 0px 0px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
|
||||||
@ -60,7 +61,7 @@ a:hover > .title {
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
background: var(--dark-three);
|
background: var(--dark-three);
|
||||||
padding: 40px;
|
padding: 30px;
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<NavbarLink link="/">home</NavbarLink>
|
<NavbarLink link="/">home</NavbarLink>
|
||||||
<NavbarLink link="/services">services</NavbarLink>
|
<NavbarLink link="/services">services</NavbarLink>
|
||||||
<NavbarLink link="/blog">blog</NavbarLink>
|
<NavbarLink link="/blog">blog</NavbarLink>
|
||||||
<NavbarLink link="/donate">donate</NavbarLink>
|
<!-- <NavbarLink link="/donate">donate</NavbarLink> -->
|
||||||
<NavbarLink link="https://stats.ngn.tf">status</NavbarLink>
|
<NavbarLink link="https://stats.ngn.tf">status</NavbarLink>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<style>
|
<style>
|
||||||
nav {
|
nav {
|
||||||
background: var(--dark-one);
|
background: var(--dark-one);
|
||||||
padding: 25px 30px 27px 25px;
|
padding: 20px 26px 22px 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -37,9 +37,9 @@ main {
|
|||||||
background: var(--dark-two);
|
background: var(--dark-two);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
|
border: solid 1px var(--border-color);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: none;
|
|
||||||
color: white;
|
color: white;
|
||||||
gap: 100px;
|
gap: 100px;
|
||||||
transition: .4s;
|
transition: .4s;
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<Card title="whoami">
|
<Card title="whoami">
|
||||||
<div class="whoami-box">
|
<div class="whoami-box">
|
||||||
<div class="whoami-pic">
|
<div class="whoami-pic">
|
||||||
<img alt="My profile pic" src="https://files.ngn.tf/pplow.png">
|
<img alt="My profile" src="https://files.ngn.tf/pplow.png">
|
||||||
<a href="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D">
|
<a href="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D">
|
||||||
<c><i class="nf nf-oct-key"></i> Keyoxide</c>
|
<c><i class="nf nf-oct-key"></i> Keyoxide</c>
|
||||||
</a>
|
</a>
|
||||||
@ -54,9 +54,9 @@
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="wall">
|
<Card title="wall">
|
||||||
Here are some links if you want to get in contact with me, please do not
|
Here are some links if you want to get in contact with me, I highly
|
||||||
use these if you want to contact about the services that I offer, I have a
|
prefer email and I usually respond to emails in 1 or 2 days, just make
|
||||||
seperate email for that: <a href="mailto:services@ngn.tf"><c>services@ngn.tf</c></a>
|
sure to check your spam folder (turns out running a TOR relay gets your IP into multiple blacklists)
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<c><i class="nf nf-cod-github"></i></c>
|
<c><i class="nf nf-cod-github"></i></c>
|
||||||
@ -64,15 +64,15 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<c><i class="nf nf-md-mastodon"></i></c>
|
<c><i class="nf nf-md-mastodon"></i></c>
|
||||||
<a href="https://mastodon.social/@ngn" rel="me">Mastodon</a>
|
<a href="https://defcon.social/@ngn" rel="me">Mastodon</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<c><i class="nf nf-md-email"></i></c>
|
<c><i class="nf nf-md-email"></i></c>
|
||||||
<a href="mailto:ngn@ngn.tf">Email (personal)</a>
|
<a href="mailto:ngn@ngn.tf">Email</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<c><i class="nf nf-md-xmpp"></i></c>
|
<c><i class="nf nf-md-xmpp"></i></c>
|
||||||
<a href="xmpp:ngn@chat.ngn.tf">XMPP (my preferred way of communication)</a>
|
<a href="xmpp:ngn@chat.ngn.tf">XMPP</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
</Card>
|
||||||
@ -80,20 +80,20 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="version">
|
<div class="version">
|
||||||
<p>v4.9</p>
|
<p>v5.0</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main{
|
main{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 35px;
|
gap: 28px;
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flexbox {
|
.flexbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 30px;
|
gap: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.whoami-box {
|
.whoami-box {
|
||||||
@ -116,10 +116,10 @@ main{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.whoami-pic img {
|
.whoami-pic img {
|
||||||
width: 250px;
|
width: 200px;
|
||||||
border-radius: 30px;
|
border-radius: 20px;
|
||||||
|
|
||||||
border: solid 2px white;
|
border: solid 1px var(--border-color);
|
||||||
animation-name: fullBorderAnimation;
|
animation-name: fullBorderAnimation;
|
||||||
animation-duration: 10s;
|
animation-duration: 10s;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
.post-list{
|
.post-list{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 35px;
|
gap: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@ -68,14 +68,15 @@ p {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 15px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feed-list a {
|
.feed-list a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 10px 18px 10px 18px;
|
padding: 10px 15px 10px 15px;
|
||||||
background: var(--dark-three);
|
background: var(--dark-three);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
border: solid 1px var(--border-color);
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
let audio
|
let audio
|
||||||
|
|
||||||
async function get_status() {
|
async function get_status() {
|
||||||
const res = await fetch(api+"/blog/vote/status?id="+data.id)
|
const res = await fetch(api+"/blog/vote/get?id="+data.id)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
|
||||||
if(json["error"]!= ""){
|
if(json["error"]!= ""){
|
||||||
@ -61,6 +61,7 @@
|
|||||||
if (voted){
|
if (voted){
|
||||||
data.vote += 2
|
data.vote += 2
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
voted = true
|
voted = true
|
||||||
data.vote += 1
|
data.vote += 1
|
||||||
@ -81,6 +82,7 @@
|
|||||||
if (voted){
|
if (voted){
|
||||||
data.vote -= 2
|
data.vote -= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
voted = true
|
voted = true
|
||||||
data.vote -= 1
|
data.vote -= 1
|
||||||
@ -139,9 +141,10 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 35px;
|
padding: 30px;
|
||||||
background: var(--dark-four);
|
background: var(--dark-four);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
border: solid 1px var(--border-color);
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
width: auto;
|
width: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<Card title="bash donate.sh">
|
<Card title="bash donate.sh">
|
||||||
I work on free/libre and open source software and offer free services. General hosting
|
I work on free/libre and open source software and offer free services. General hosting
|
||||||
and stuff costs around 550₺ (~$17), so feel free to donate in order to help me keep
|
and stuff costs around 550₺ (~$17) per month, so feel free to donate in order to help me keep
|
||||||
everything up and running!
|
everything up and running!
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
@ -35,6 +35,8 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
Also huge thanks to all of you who has donated so far, even if it's a small amount, I highly
|
||||||
|
appreciate it. Thank you!
|
||||||
</Card>
|
</Card>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ table {
|
|||||||
color: white;
|
color: white;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 30px 0 0 0;
|
margin: 30px 0 30px 0;
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,9 +32,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<c><i class="nf nf-cod-account"></i> Registration:</c> All the services are offered for free, and all of them
|
<c><i class="nf nf-cod-account"></i> Registration:</c> All the services are offered for free, and all of them
|
||||||
are accessiable to public. If you want to use a service that requires
|
are accessiable to public. And registrations are open for the all services that support account registrations.
|
||||||
registration, you can email <a href="mailto:services@ngn.tf"><c>services@ngn.tf</c></a>
|
|
||||||
for an account.
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<c><i class="nf nf-fa-eye_slash"></i> Privacy:</c> To protect user privacy, all the web proxy logs are cleared regularly.
|
<c><i class="nf nf-fa-eye_slash"></i> Privacy:</c> To protect user privacy, all the web proxy logs are cleared regularly.
|
||||||
@ -64,7 +62,7 @@ main {
|
|||||||
align-content: center;
|
align-content: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
gap: 30px;
|
gap: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flexcol {
|
.flexcol {
|
||||||
@ -72,7 +70,7 @@ main {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 20px;
|
gap: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flexrow {
|
.flexrow {
|
||||||
@ -81,11 +79,7 @@ main {
|
|||||||
align-content: center;
|
align-content: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 20px;
|
gap: 13px;
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
--text-shadow: 0px 10px 20px rgba(90, 90, 90, 0.8);
|
--text-shadow: 0px 10px 20px rgba(90, 90, 90, 0.8);
|
||||||
--box-shadow: rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px;
|
--box-shadow: rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px;
|
||||||
|
--border-color: #2f2f2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user