From 363f557c35e6ebcd63bbc764a63e244a241617cd Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Sat, 12 Oct 2024 18:39:38 +0300 Subject: [PATCH] Replace JSON with SQLite --- .gitignore | 2 +- README.md | 6 ++--- auth.go | 31 ++++++++++++---------- db.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ jwt.go | 3 +-- main.go | 4 ++- pseudo_db.go | 68 ------------------------------------------------ user.go | 7 +---- 10 files changed, 101 insertions(+), 96 deletions(-) create mode 100644 db.go delete mode 100644 pseudo_db.go diff --git a/.gitignore b/.gitignore index c9a0010..27f7388 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ pye-auth private.key -data.json \ No newline at end of file +data.db \ No newline at end of file diff --git a/README.md b/README.md index 6c999d9..14f190b 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file diff --git a/auth.go b/auth.go index cc2cf3f..be7825f 100644 --- a/auth.go +++ b/auth.go @@ -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 diff --git a/db.go b/db.go new file mode 100644 index 0000000..d720bc6 --- /dev/null +++ b/db.go @@ -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 +} diff --git a/go.mod b/go.mod index 8675dd1..6945d55 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index b6a6062..67dd0db 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/jwt.go b/jwt.go index c6dcc5e..c8fb916 100644 --- a/jwt.go +++ b/jwt.go @@ -14,9 +14,8 @@ import ( "github.com/golang-jwt/jwt/v5" ) -var KeyFile = "private.key" - var ( + KeyFile = "private.key" key *rsa.PrivateKey ) diff --git a/main.go b/main.go index 7ee58f0..08969f7 100644 --- a/main.go +++ b/main.go @@ -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) } diff --git a/pseudo_db.go b/pseudo_db.go deleted file mode 100644 index a7837ce..0000000 --- a/pseudo_db.go +++ /dev/null @@ -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 -} diff --git a/user.go b/user.go index b10e514..abbfb7a 100644 --- a/user.go +++ b/user.go @@ -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) -} +} \ No newline at end of file