summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/exampleReq.go59
-rw-r--r--api/main.go42
-rw-r--r--api/utils.go29
-rw-r--r--cmd/redq/main.go17
-rw-r--r--cmd/redqctl/main.go74
-rw-r--r--db/account.go112
-rw-r--r--db/bearer.go127
-rw-r--r--db/main.go74
-rw-r--r--db/utils.go24
-rw-r--r--flake.lock27
-rw-r--r--flake.nix25
-rw-r--r--go.mod13
-rw-r--r--go.sum9
13 files changed, 632 insertions, 0 deletions
diff --git a/api/exampleReq.go b/api/exampleReq.go
new file mode 100644
index 0000000..d4ec862
--- /dev/null
+++ b/api/exampleReq.go
@@ -0,0 +1,59 @@
+package api
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+
+ redqdb "sinanmohd.com/redq/db"
+)
+
+type examplApiName struct {
+ db *redqdb.SafeDB
+ req *RequestApiName
+ resp *ResponseApiName
+}
+
+type RequestApiName struct {
+ BearerToken string
+}
+
+type ResponseApiName struct {
+ Bearer *redqdb.Bearer
+ Error string
+}
+
+func newExamplApiName(db *redqdb.SafeDB) *examplApiName {
+ a := &examplApiName{}
+ a.db = db
+
+ return a
+}
+
+func (a *examplApiName) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+ a.req = &RequestApiName{}
+ a.resp = &ResponseApiName{}
+ a.resp.Bearer = &redqdb.Bearer{}
+
+ err := unmarshal(r.Body, a.req)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ err = a.resp.Bearer.VerifyAndUpdate(a.db, a.req.BearerToken)
+ if err != nil {
+ log.Println(err)
+ a.resp.Error = err.Error()
+ return
+ }
+
+ json, err := marshal(a.resp)
+ if err != nil {
+ log.Println(err)
+ a.resp.Error = err.Error()
+ return
+ }
+
+ fmt.Fprintf(rw, json)
+}
diff --git a/api/main.go b/api/main.go
new file mode 100644
index 0000000..29f71c9
--- /dev/null
+++ b/api/main.go
@@ -0,0 +1,42 @@
+package api
+
+import (
+ "fmt"
+ "net/http"
+
+ redqdb "sinanmohd.com/redq/db"
+)
+
+func Run(db *redqdb.SafeDB) {
+ const prefix string = "POST /_redq/api"
+
+ exampleApi := newExamplApiName(db)
+ http.Handle(prefix+"/example", exampleApi)
+
+ http.HandleFunc("GET /{$}", home)
+ http.ListenAndServe(":8008", nil)
+}
+
+func home(rw http.ResponseWriter, r *http.Request) {
+ const index string = `
+ <!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>🚨 redq</title>
+ </head>
+ <body>
+ <center>
+ <h1 style="font-size: 10em">
+ redq is active
+ </h1>
+ <p style="font-weight: bold">
+ we're soo back 🥳
+ </p>
+ </center>
+ </body>
+ </html>
+ `
+
+ fmt.Fprint(rw, index)
+}
diff --git a/api/utils.go b/api/utils.go
new file mode 100644
index 0000000..40b41e6
--- /dev/null
+++ b/api/utils.go
@@ -0,0 +1,29 @@
+package api
+
+import (
+ "encoding/json"
+ "io"
+)
+
+func unmarshal(r io.Reader, v any) error {
+ body, err := io.ReadAll(r)
+ if err != nil {
+ return err
+ }
+
+ err = json.Unmarshal(body, v)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func marshal(v any) (string, error) {
+ b, err := json.Marshal(v)
+ if err != nil {
+ return "", err
+ }
+
+ return string(b), nil
+}
diff --git a/cmd/redq/main.go b/cmd/redq/main.go
new file mode 100644
index 0000000..1f153d3
--- /dev/null
+++ b/cmd/redq/main.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+ "log"
+
+ redqapi "sinanmohd.com/redq/api"
+ redqdb "sinanmohd.com/redq/db"
+)
+
+func main() {
+ db, err := redqdb.NewSafeDB()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ redqapi.Run(db)
+}
diff --git a/cmd/redqctl/main.go b/cmd/redqctl/main.go
new file mode 100644
index 0000000..f6f9e9e
--- /dev/null
+++ b/cmd/redqctl/main.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+
+ redqdb "sinanmohd.com/redq/db"
+)
+
+func help() {
+ const helpString string =
+`redqctl is a tool for managing redq.
+
+Usage:
+
+ redqctl <command> [arguments]
+
+The commands are:
+
+ create create a redq account
+ help show this help cruft
+
+`
+
+ fmt.Print(helpString)
+}
+
+func create(args []string, db *redqdb.SafeDB) {
+ f := flag.NewFlagSet("create", flag.ExitOnError)
+ ac := &redqdb.Account{}
+ ac.Info = &redqdb.Login{}
+
+ f.StringVar(&ac.Email, "email", "",
+ "The email to associate with the account")
+ f.StringVar(&ac.Info.FirstName, "fname", "",
+ "The first name to associate with the account")
+ f.StringVar(&ac.Info.LastName, "lname", "",
+ "The last name to associate with the account")
+ f.StringVar(&ac.PassHash, "pass", "",
+ "The password to associate with the account")
+ f.UintVar(&ac.Info.Level, "level", 0,
+ "The level to associate with the account")
+ f.Parse(args)
+
+ err := ac.CreateAccount(db)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func main() {
+ args := os.Args[1:]
+ if len(args) == 0 {
+ help()
+ os.Exit(2)
+ }
+
+ db, err := redqdb.NewSafeDB()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ switch args[0] {
+ case "help":
+ help()
+ case "create":
+ create(args[1:], db)
+ default:
+ help()
+ os.Exit(2)
+ }
+}
diff --git a/db/account.go b/db/account.go
new file mode 100644
index 0000000..2c99045
--- /dev/null
+++ b/db/account.go
@@ -0,0 +1,112 @@
+package db
+
+import "errors"
+
+type Account struct {
+ Email string
+ PassHash string
+
+ Info *Login
+}
+
+type Login struct {
+ id uint
+ Level uint
+ FirstName, LastName string
+ Bearer *Bearer
+}
+
+func (ac *Account) CreateAccount(safe *SafeDB) error {
+ const sqlStatement string = `
+ INSERT INTO Accounts (
+ id,
+ Email,
+ PassHash,
+ Level,
+ FirstName,
+ LastName
+ )
+ VALUES (NULL, ?, ?, ?, ?, ?);
+ `
+
+ safe.mu.Lock()
+ defer safe.mu.Unlock()
+
+ _, err := safe.db.Exec(
+ sqlStatement,
+ ac.Email,
+ ToBlake3(ac.PassHash),
+
+ ac.Info.FirstName,
+ ac.Info.LastName,
+ ac.Info.Level,
+ )
+
+ return err
+}
+
+func (ac *Account) Login(safe *SafeDB) error {
+ const sqlStatementQuery string = `
+ SELECT id, PassHash, Level, FirstName, LastName
+ FROM Accounts
+ WHERE Accounts.Email = ?
+ `
+
+ ac.Info = &Login{}
+ ac.Info.Bearer = &Bearer{}
+ safe.mu.Lock()
+ row := safe.db.QueryRow(sqlStatementQuery, ac.Email)
+ safe.mu.Unlock()
+
+ var PassHash string
+ err := row.Scan(
+ &ac.Info.id,
+ &PassHash,
+ &ac.Info.FirstName,
+ &ac.Info.LastName,
+ &ac.Info.Level,
+ )
+ if err != nil {
+ return err
+ }
+ if PassHash != ac.PassHash {
+ return errors.New("Auth failed")
+ }
+
+ err = ac.Info.Bearer.Generate(safe, ac.Info)
+ if err != nil {
+ return err
+ }
+
+ return err
+}
+
+func (ac *Account) fromBearer(safe *SafeDB, b *Bearer) error {
+ const sqlStatementAccount string = `
+ SELECT Email, PassHash, Level, FirstName, LastName
+ FROM Accounts
+ WHERE Accounts.id = ?
+ `
+
+ safe.mu.Lock()
+ row := safe.db.QueryRow(sqlStatementAccount, b.accountId)
+ safe.mu.Unlock()
+
+ ac.Info = &Login{}
+ ac.Info.id = b.accountId
+ ac.Info.Bearer = b
+ err := row.Scan(
+ &ac.Email,
+ &ac.PassHash,
+
+ &ac.Info.FirstName,
+ &ac.Info.LastName,
+ &ac.Info.Level,
+ )
+ if err != nil {
+ return err
+ }
+ ac.Info.Bearer = b
+
+ return err
+}
diff --git a/db/bearer.go b/db/bearer.go
new file mode 100644
index 0000000..b16d506
--- /dev/null
+++ b/db/bearer.go
@@ -0,0 +1,127 @@
+package db
+
+import (
+ "errors"
+ "time"
+
+ _ "github.com/mattn/go-sqlite3"
+)
+
+type Bearer struct {
+ id, accountId uint
+ Token string
+ ValidUpTo time.Time
+}
+
+func (b *Bearer) FromToken(safe *SafeDB, Token string) error {
+ const sqlStatementBearer string = `
+ SELECT id, ValidUpTo, accountId
+ FROM Bearer
+ WHERE Bearer.Token = ?
+ `
+
+ b.Token = Token
+ var ValidUpToString string
+ safe.mu.Lock()
+ row := safe.db.QueryRow(sqlStatementBearer, Token)
+ safe.mu.Unlock()
+
+ err := row.Scan(
+ &b.id,
+ &ValidUpToString,
+ &b.accountId,
+ )
+ if err != nil {
+ return err
+ }
+
+ layout := "2006-01-02 15:04:05.999999999-07:00"
+ b.ValidUpTo, err = time.Parse(layout, ValidUpToString)
+ if err != nil {
+ return err
+ }
+
+ timeNow := time.Now()
+ if timeNow.After(b.ValidUpTo) {
+ return errors.New("Outdated Bearer Token")
+ }
+
+ return err
+}
+
+func (b *Bearer) Update(safe *SafeDB) error {
+ const sqlStatementBearer string = `
+ UPDATE Bearer
+ SET ValidUpTo = ?
+ WHERE id = ?
+ `
+
+ validUpTo := time.Now().Add(time.Hour * 24)
+ safe.mu.Lock()
+ _, err := safe.db.Exec(sqlStatementBearer, validUpTo, b.id)
+ safe.mu.Unlock()
+ if err != nil {
+ return err
+ }
+ b.ValidUpTo = validUpTo
+
+ return nil
+}
+
+func (b *Bearer) VerifyAndUpdate(safe *SafeDB, token string) error {
+ err := b.FromToken(safe, token)
+ if err != nil {
+ return err
+ }
+
+ err = b.Update(safe)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (b *Bearer) Generate(safe *SafeDB, lg *Login) error {
+ const sqlGenBearer string = `
+ INSERT INTO Bearer (
+ id,
+ Token,
+ ValidUpTo,
+ accountId
+ )
+ VALUES (NULL, ?, ?, ?);
+ `
+
+ Token, err := GenRandomString(128)
+ if err != nil {
+ return err
+ }
+
+ timeNow := time.Now()
+ ValidUpTo := timeNow.Add(time.Hour * 24)
+ safe.mu.Lock()
+ res, err := safe.db.Exec(
+ sqlGenBearer,
+ Token,
+ ValidUpTo,
+ lg.id,
+ )
+ safe.mu.Unlock()
+ if err != nil {
+ return err
+ }
+
+ id, err := res.LastInsertId()
+ if err != nil {
+ return err
+ }
+
+ b.id = uint(id)
+ b.accountId = lg.id
+ b.Token = Token
+ b.ValidUpTo = ValidUpTo
+ lg.Bearer = b
+
+ return err
+}
diff --git a/db/main.go b/db/main.go
new file mode 100644
index 0000000..c78ea3a
--- /dev/null
+++ b/db/main.go
@@ -0,0 +1,74 @@
+package db
+
+import (
+ "database/sql"
+ "os"
+ "path/filepath"
+ "sync"
+
+ _ "github.com/mattn/go-sqlite3"
+)
+
+type SafeDB struct {
+ mu sync.Mutex
+
+ path string
+ db *sql.DB
+}
+
+func (safe *SafeDB) setupPath() error {
+ const path string = "/var/lib/redq/"
+ const name string = "redq.sqlite3"
+
+ err := os.MkdirAll(path, os.ModeDir)
+ if err != nil {
+ return err
+ }
+
+ safe.path = filepath.Join(path, name)
+ return nil
+}
+
+func NewSafeDB() (*SafeDB, error) {
+ const create string = `
+ CREATE TABLE IF NOT EXISTS Accounts(
+ id INTEGER PRIMARY KEY,
+ Email CHAR(64) NOT NULL UNIQUE,
+ PassHash CHAR(128) NOT NULL,
+
+ Level INTEGER NOT NULL,
+ FirstName CHAR(32) NOT NULL,
+ LastName CHAR(32) NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS Bearer(
+ id INTEGER PRIMARY KEY,
+ Token CHAR(128) NOT NULL UNIQUE,
+ ValidUpTo TIME NOT NULL,
+ accountId INTEGER NOT NULL,
+
+ FOREIGN KEY (accountId)
+ REFERENCES Accounts (id)
+ );
+ `
+ safe := &SafeDB{}
+ err := safe.setupPath()
+ if err != nil {
+ return nil, err
+ }
+
+ safe.mu.Lock()
+ defer safe.mu.Unlock()
+
+ safe.db, err = sql.Open("sqlite3", safe.path)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = safe.db.Exec(create)
+ if err != nil {
+ return nil, err
+ }
+
+ return safe, nil
+}
diff --git a/db/utils.go b/db/utils.go
new file mode 100644
index 0000000..0b0f1cb
--- /dev/null
+++ b/db/utils.go
@@ -0,0 +1,24 @@
+package db
+
+import (
+ "encoding/base64"
+ "lukechampine.com/blake3"
+ "math/rand"
+)
+
+func ToBlake3(pass string) string {
+ hash := blake3.Sum512([]byte(pass))
+ hash64b := base64.StdEncoding.EncodeToString(hash[:])
+
+ return "blake3-" + hash64b
+}
+
+func GenRandomString(n int) (string, error) {
+ b := make([]byte, n)
+ _, err := rand.Read(b)
+ if err != nil {
+ return "", err
+ }
+
+ return base64.URLEncoding.EncodeToString(b)[:n], nil
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..6b7d91d
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1709479366,
+ "narHash": "sha256-n6F0n8UV6lnTZbYPl1A9q1BS0p4hduAv1mGAP17CVd0=",
+ "owner": "NixOs",
+ "repo": "nixpkgs",
+ "rev": "b8697e57f10292a6165a20f03d2f42920dfaf973",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOs",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..813d633
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,25 @@
+{
+ inputs.nixpkgs.url = "github:NixOs/nixpkgs/nixos-unstable";
+
+ outputs = { self, nixpkgs }: let
+ lib = nixpkgs.lib;
+
+ supportedSystems = lib.platforms.unix;
+ forSystem = f: system: f {
+ inherit system;
+ pkgs = import nixpkgs { inherit system; };
+ };
+ forAllSystems = f: lib.genAttrs supportedSystems (forSystem f);
+ in {
+ devShells = forAllSystems ({ system, pkgs, ... }: {
+ default = pkgs.mkShell {
+ name = "dev";
+
+ buildInputs = with pkgs; [ go_1_22 gopls jq ];
+ shellHook = ''
+ export PS1="\033[0;36m[ ]\033[0m $PS1"
+ '';
+ };
+ });
+ };
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c18c5e2
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,13 @@
+module sinanmohd.com/redq
+
+go 1.22.0
+
+require (
+ github.com/mattn/go-sqlite3 v1.14.22
+ lukechampine.com/blake3 v1.2.1
+)
+
+require (
+ github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+ golang.org/x/sys v0.8.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..b2786bb
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,9 @@
+github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
+lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=