Replace JSON with SQLite

This commit is contained in:
Andrew-71 2024-10-12 18:39:38 +03:00
parent 170416e791
commit 363f557c35
10 changed files with 101 additions and 96 deletions

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
pye-auth
private.key
data.json
data.db

View file

@ -9,14 +9,12 @@ with(out) blazingly fast cloud-native web3 memory-safe blockchain reactive AI
This should be done by **October 17th 2024**. Or, at the very least,
in a state that proves I am competent Go developer.
Note: **JSON** is used for storage at proof-of-concept stage for ease of use,
obviously I'd use **SQL** for production
## Current functionality
* Port `7102`
* `POST /register` - register a user with Basic Auth
* `POST /login` - get a JWT token by Basic Auth
* `GET /pem` - get PEM-encoded public RS256 key
* Data persistently stored in... `data.json`, for convenience
* Data persistently stored in an SQLite database `data.db`
(requires creation of empty db)
* RS256 key loaded from `private.key` file or generated on startup if missing

31
auth.go
View file

@ -7,38 +7,41 @@ import (
"strings"
)
func ValidEmail(email string) bool {
func validEmail(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}
func ValidPass(pass string) bool {
func validPass(pass string) bool {
// TODO: Obviously, we *might* want something more sophisticated here
return len(pass) >= 8
}
func EmailTaken(email string) bool {
// FIXME: Implement properly
return EmailExists(email)
}
func Register(w http.ResponseWriter, r *http.Request) {
email, password, ok := r.BasicAuth()
if ok {
email = strings.TrimSpace(email)
password = strings.TrimSpace(password)
if !(ValidEmail(email) && ValidPass(password) && !EmailTaken(email)) {
slog.Info("Outcome",
"email", ValidEmail(email),
"pass", ValidPass(password),
"taken", !EmailTaken(email))
if !(validEmail(email) && validPass(password) && !emailExists(email)) {
slog.Debug("Outcome",
"email", validEmail(email),
"pass", validPass(password),
"taken", !emailExists(email))
http.Error(w, "invalid auth credentials", http.StatusBadRequest)
return
}
user, err := NewUser(email, password)
if err != nil {
slog.Error("error creating a new user", "error", err)
http.Error(w, "error creating a new user", http.StatusInternalServerError)
return
}
err = addUser(user)
if err != nil {
slog.Error("error saving a new user", "error", err)
http.Error(w, "error saving a new user", http.StatusInternalServerError)
return
}
slog.Info("user", "user", user)
AddUser(user)
w.WriteHeader(http.StatusCreated)
w.Write([]byte("User created"))
return
@ -55,7 +58,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
if ok {
email = strings.TrimSpace(email)
password = strings.TrimSpace(password)
user, ok := ByEmail(email)
user, ok := byEmail(email)
if !ok || !user.PasswordFits(password) {
http.Error(w, "you did something wrong", http.StatusUnauthorized)
return

73
db.go Normal file
View file

@ -0,0 +1,73 @@
package main
import (
"database/sql"
"errors"
"log/slog"
"os"
_ "github.com/mattn/go-sqlite3"
)
const create string = `
CREATE TABLE "users" (
"uuid" TEXT NOT NULL UNIQUE,
"email" TEXT NOT NULL UNIQUE,
"password" TEXT NOT NULL,
PRIMARY KEY("uuid")
);`
var (
DataFile = "data.db"
db *sql.DB = LoadDb()
)
func LoadDb() *sql.DB {
// I *think* we need some file, even if only empty
if _, err := os.Stat(DataFile); errors.Is(err, os.ErrNotExist) {
slog.Error("sqlite3 database file required", "file", DataFile)
os.Exit(1)
}
db, err := sql.Open("sqlite3", DataFile)
if err != nil {
slog.Error("error opening database", "error", err)
os.Exit(1)
}
if _, err := db.Exec(create); err != nil && err.Error() != "table \"users\" already exists" {
slog.Info("error initialising database table", "error", err)
os.Exit(1)
}
slog.Info("loaded database")
return db
}
func addUser(user User) error {
_, err := db.Exec("insert into users (uuid, email, password) values ($1, $2, $3)",
user.Uuid.String(), user.Email, user.Hash)
if err != nil {
slog.Error("error adding user", "error", err, "user", user)
return err
}
return nil
}
func byId(uuid string) (User, bool) {
row := db.QueryRow("select * from users where uuid = $1", uuid)
user := User{}
err := row.Scan(&user.Uuid, &user.Email, &user.Hash)
return user, err == nil
}
func byEmail(email string) (User, bool) {
row := db.QueryRow("select * from users where email = $1", email)
user := User{}
err := row.Scan(&user.Uuid, &user.Email, &user.Hash)
return user, err == nil
}
func emailExists(email string) bool {
_, ok := byEmail(email)
return ok
}

1
go.mod
View file

@ -5,5 +5,6 @@ go 1.22
require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/crypto v0.28.0
)

2
go.sum
View file

@ -2,5 +2,7 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=

3
jwt.go
View file

@ -14,9 +14,8 @@ import (
"github.com/golang-jwt/jwt/v5"
)
var KeyFile = "private.key"
var (
KeyFile = "private.key"
key *rsa.PrivateKey
)

View file

@ -15,7 +15,9 @@ func main() {
router.HandleFunc("POST /register", Register)
router.HandleFunc("POST /login", Login)
router.HandleFunc("GET /login", Login) // TODO: temp
// Note: likely temporary, possibly to be replaced by a fake "frontend"
router.HandleFunc("GET /login", Login)
router.HandleFunc("GET /register", Register)
http.ListenAndServe(":7102", router)
}

View file

@ -1,68 +0,0 @@
package main
import (
"encoding/json"
"log/slog"
"os"
)
// SQLite seems to hate my Mac.
// And I'd rather deal with something easily tinker-able in PoC stage
// So.................
// JSON.
//
// TODO: Kill this, preferably with fire.
func ReadUsers() []User {
data, err := os.ReadFile("./data.json")
if err != nil {
slog.Error("error reading file", "error", err)
}
var users []User
err = json.Unmarshal(data, &users)
if err != nil {
slog.Error("error unmarshalling data", "error", err)
}
return users
}
func AddUser(user User) {
users := ReadUsers()
users = append(users, user)
// slog.Info("users", "users", users)
data, err := json.Marshal(users)
if err != nil {
slog.Error("error marshalling", "error", err)
return
}
f, err := os.OpenFile("./data.json", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
slog.Error("error opening/creating file data.json")
return
}
if _, err := f.Write(data); err != nil {
slog.Error("error writing to file data.json", "error", err)
return
}
}
func EmailExists(email string) bool {
users := ReadUsers()
for i := 0; i < len(users); i++ {
if users[i].Email == email {
return true
}
}
return false
}
func UserByEmail(email string) (User, bool) {
users := ReadUsers()
for i := 0; i < len(users); i++ {
if users[i].Email == email {
return users[i], true
}
}
return User{}, false
}

View file

@ -22,9 +22,4 @@ func NewUser(email, password string) (User, error) {
return User{}, err
}
return User{uuid.New(), email, hash}, nil
}
// TODO: Implement
func ByEmail(email string) (User, bool) {
return UserByEmail(email)
}
}