diff --git a/.gitignore b/.gitignore index 27f7388..ec723ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -pye-auth +pye private.key data.db \ No newline at end of file diff --git a/Makefile b/Makefile index 62e8f1d..d0aed03 100644 --- a/Makefile +++ b/Makefile @@ -2,4 +2,4 @@ build: go build run: - go build && ./pye-auth \ No newline at end of file + go build && ./pye \ No newline at end of file diff --git a/auth.go b/auth.go index be7825f..c7114be 100644 --- a/auth.go +++ b/auth.go @@ -22,24 +22,18 @@ func Register(w http.ResponseWriter, r *http.Request) { if ok { email = strings.TrimSpace(email) password = strings.TrimSpace(password) - if !(validEmail(email) && validPass(password) && !emailExists(email)) { + if !(validEmail(email) && validPass(password) && !data.EmailExists(email)) { slog.Debug("Outcome", "email", validEmail(email), "pass", validPass(password), - "taken", !emailExists(email)) + "taken", !data.EmailExists(email)) http.Error(w, "invalid auth credentials", http.StatusBadRequest) return } - user, err := NewUser(email, password) + err := data.AddUser(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) + slog.Error("error adding a new user", "error", err) + http.Error(w, "error adding a new user", http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) @@ -58,8 +52,9 @@ func Login(w http.ResponseWriter, r *http.Request) { if ok { email = strings.TrimSpace(email) password = strings.TrimSpace(password) - user, ok := byEmail(email) + user, ok := data.ByEmail(email) if !ok || !user.PasswordFits(password) { + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) http.Error(w, "you did something wrong", http.StatusUnauthorized) return } diff --git a/go.mod b/go.mod index 6945d55..f1a85ee 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module pye-auth +module git.a71.su/Andrew71/pye go 1.22 diff --git a/jwt.go b/jwt.go index c8fb916..749560e 100644 --- a/jwt.go +++ b/jwt.go @@ -11,17 +11,18 @@ import ( "os" "time" + "git.a71.su/Andrew71/pye/storage" "github.com/golang-jwt/jwt/v5" ) var ( - KeyFile = "private.key" - key *rsa.PrivateKey + KeyFile = "private.key" + key *rsa.PrivateKey ) // LoadKey attempts to load a private key from KeyFile. // If the file does not exist, it generates a new key (and saves it) -func LoadKey() { +func MustLoadKey() { // If the key doesn't exist, create it if _, err := os.Stat(KeyFile); errors.Is(err, os.ErrNotExist) { key, err = rsa.GenerateKey(rand.Reader, 4096) @@ -59,6 +60,10 @@ func LoadKey() { } } +func init() { + MustLoadKey() +} + // publicKey returns our public key as PEM block func publicKey(w http.ResponseWriter, r *http.Request) { key_marshalled := x509.MarshalPKCS1PublicKey(&key.PublicKey) @@ -66,7 +71,7 @@ func publicKey(w http.ResponseWriter, r *http.Request) { pem.Encode(w, &block) } -func CreateJWT(usr User) (string, error) { +func CreateJWT(usr storage.User) (string, error) { t := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ "iss": "pye", @@ -99,7 +104,3 @@ func VerifyJWT(token string, publicKey []byte) bool { slog.Info("Error check", "err", err) return err == nil } - -func init() { - LoadKey() -} diff --git a/main.go b/main.go index 08969f7..1ef5c1e 100644 --- a/main.go +++ b/main.go @@ -3,8 +3,13 @@ package main import ( "fmt" "net/http" + + "git.a71.su/Andrew71/pye/storage" + "git.a71.su/Andrew71/pye/storage/sqlite" ) +var data storage.Storage = sqlite.MustLoadSQLite() + func main() { fmt.Println("=== Working on port 7102 ===") @@ -16,7 +21,7 @@ func main() { router.HandleFunc("POST /login", Login) // Note: likely temporary, possibly to be replaced by a fake "frontend" - router.HandleFunc("GET /login", Login) + router.HandleFunc("GET /login", Login) router.HandleFunc("GET /register", Register) http.ListenAndServe(":7102", router) diff --git a/db.go b/storage/sqlite/sqlite.go similarity index 53% rename from db.go rename to storage/sqlite/sqlite.go index d720bc6..79bf614 100644 --- a/db.go +++ b/storage/sqlite/sqlite.go @@ -1,4 +1,4 @@ -package main +package sqlite import ( "database/sql" @@ -6,6 +6,7 @@ import ( "log/slog" "os" + "git.a71.su/Andrew71/pye/storage" _ "github.com/mattn/go-sqlite3" ) @@ -18,11 +19,49 @@ const create string = ` );` var ( - DataFile = "data.db" - db *sql.DB = LoadDb() + DataFile = "data.db" ) -func LoadDb() *sql.DB { +type SQLiteStorage struct { + db *sql.DB +} + +func (s SQLiteStorage) AddUser(email, password string) error { + user, err := storage.NewUser(email, password) + if err != nil { + return err + } + _, err = s.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 to database", "error", err, "user", user) + return err + } + return nil +} + +func (s SQLiteStorage) ById(uuid string) (storage.User, bool) { + row := s.db.QueryRow("select * from users where uuid = $1", uuid) + user := storage.User{} + err := row.Scan(&user.Uuid, &user.Email, &user.Hash) + + return user, err == nil +} + +func (s SQLiteStorage) ByEmail(email string) (storage.User, bool) { + row := s.db.QueryRow("select * from users where email = $1", email) + user := storage.User{} + err := row.Scan(&user.Uuid, &user.Email, &user.Hash) + + return user, err == nil +} + +func (s SQLiteStorage) EmailExists(email string) bool { + _, ok := s.ByEmail(email) + return ok +} + +func MustLoadSQLite() SQLiteStorage { // 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) @@ -33,41 +72,12 @@ func LoadDb() *sql.DB { slog.Error("error opening database", "error", err) os.Exit(1) } + + // TODO: Apparently "prepare" works here 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 + return SQLiteStorage{db} } diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..dcee5fe --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,8 @@ +package storage + +type Storage interface { + AddUser(email, password string) error + ById(uuid string) (User, bool) + ByEmail(uuid string) (User, bool) + EmailExists(email string) bool +} diff --git a/user.go b/storage/user.go similarity index 85% rename from user.go rename to storage/user.go index abbfb7a..5c34a5d 100644 --- a/user.go +++ b/storage/user.go @@ -1,6 +1,8 @@ -package main +package storage import ( + "log/slog" + "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) @@ -19,7 +21,8 @@ func (u User) PasswordFits(password string) bool { func NewUser(email, password string) (User, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), 14) if err != nil { + slog.Error("error creating a new user", "error", err) return User{}, err } return User{uuid.New(), email, hash}, nil -} \ No newline at end of file +}