diff --git a/README.md b/README.md index 414146a..e873a73 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,11 @@ -# Auth microservice +# Pye Auth **Mission**: Science compels us to create a microservice! -This is the repository for my **JWT auth microservice assignment** +This is the repository for my **JWT authentication microservice** with(out) blazingly fast cloud-native web3 memory-safe blockchain reactive AI (insert a dozen more buzzwords of your choosing) technologies. -This should be done by **October 17th 2024**. Or, at the very least, -in a state that proves I am competent Go developer. - ## Usage ``` @@ -33,6 +30,7 @@ Use "pye [command] --help" for more information about a command. ## Technologies used -* **Storage** - [SQLite](https://github.com/mattn/go-sqlite3) and a PEM file +* **Storage** - [SQLite](https://github.com/mattn/go-sqlite3) and a +[PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) file * **HTTP routing** - [Chi](https://go-chi.io), just for logging... * **CLI management** - [Cobra](https://cobra.dev/) \ No newline at end of file diff --git a/cmd/find_user.go b/internal/app/find.go similarity index 85% rename from cmd/find_user.go rename to internal/app/find.go index c852ea0..df7fc61 100644 --- a/cmd/find_user.go +++ b/internal/app/find.go @@ -1,9 +1,10 @@ -package cmd +package app import ( "fmt" - "git.a71.su/Andrew71/pye/storage" + "git.a71.su/Andrew71/pye/internal/models/user" + "git.a71.su/Andrew71/pye/internal/storage" "github.com/spf13/cobra" ) @@ -20,7 +21,7 @@ var findUserCmd = &cobra.Command{ } func findUser(cmd *cobra.Command, args []string) { - var user storage.User + var user user.User var ok bool if args[0] == "email" { user, ok = storage.Data.ByEmail(args[1]) diff --git a/cmd/root.go b/internal/app/root.go similarity index 73% rename from cmd/root.go rename to internal/app/root.go index c8f97ff..cc5961c 100644 --- a/cmd/root.go +++ b/internal/app/root.go @@ -1,21 +1,21 @@ -package cmd +package app import ( "fmt" "os" - "git.a71.su/Andrew71/pye/auth" - "git.a71.su/Andrew71/pye/config" - "git.a71.su/Andrew71/pye/logging" - "git.a71.su/Andrew71/pye/storage" - "git.a71.su/Andrew71/pye/storage/sqlite" + "git.a71.su/Andrew71/pye/internal/auth" + "git.a71.su/Andrew71/pye/internal/config" + "git.a71.su/Andrew71/pye/internal/logging" + "git.a71.su/Andrew71/pye/internal/storage" + "git.a71.su/Andrew71/pye/internal/storage/sqlite" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ Use: "pye", Short: "Pye is a simple JWT system", - Long: `A bare-bones authentication system built by Andrew71 as an assignment`, + Long: `A bare-bones authentication system with RS256`, } var ( diff --git a/cmd/serve.go b/internal/app/serve.go similarity index 90% rename from cmd/serve.go rename to internal/app/serve.go index 7eb409b..07f198b 100644 --- a/cmd/serve.go +++ b/internal/app/serve.go @@ -1,12 +1,12 @@ -package cmd +package app import ( "log/slog" "net/http" "strconv" - "git.a71.su/Andrew71/pye/auth" - "git.a71.su/Andrew71/pye/config" + "git.a71.su/Andrew71/pye/internal/auth" + "git.a71.su/Andrew71/pye/internal/config" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/spf13/cobra" diff --git a/cmd/verify.go b/internal/app/verify.go similarity index 95% rename from cmd/verify.go rename to internal/app/verify.go index 8492897..081c288 100644 --- a/cmd/verify.go +++ b/internal/app/verify.go @@ -1,11 +1,11 @@ -package cmd +package app import ( "fmt" "log/slog" "os" - "git.a71.su/Andrew71/pye/auth" + "git.a71.su/Andrew71/pye/internal/auth" "github.com/golang-jwt/jwt/v5" "github.com/spf13/cobra" ) diff --git a/auth/auth.go b/internal/auth/auth.go similarity index 85% rename from auth/auth.go rename to internal/auth/auth.go index f38c586..657b169 100644 --- a/auth/auth.go +++ b/internal/auth/auth.go @@ -1,12 +1,13 @@ package auth import ( + "errors" "log/slog" "net/http" "net/mail" "strings" - "git.a71.su/Andrew71/pye/storage" + "git.a71.su/Andrew71/pye/internal/storage" ) func validEmail(email string) bool { @@ -25,7 +26,7 @@ func Register(w http.ResponseWriter, r *http.Request) { if ok { email = strings.TrimSpace(email) password = strings.TrimSpace(password) - if !(validEmail(email) && validPass(password) && !storage.Data.Taken(email)) { + if !(validEmail(email) && validPass(password)) { slog.Debug("outcome", "valid_email", validEmail(email), "valid_pass", validPass(password), @@ -35,12 +36,15 @@ func Register(w http.ResponseWriter, r *http.Request) { } err := storage.Data.Add(email, password) if err != nil { - slog.Error("error adding a new user", "error", err) - http.Error(w, "error adding a new user", http.StatusInternalServerError) + if errors.Is(err, storage.ErrExist) { + http.Error(w, "invalid auth credentials", http.StatusBadRequest) + } else { + http.Error(w, "error adding a new user", http.StatusInternalServerError) + } return } w.WriteHeader(http.StatusCreated) - w.Write([]byte("User created")) + w.Write([]byte("user created")) return } diff --git a/auth/jwt.go b/internal/auth/jwt.go similarity index 95% rename from auth/jwt.go rename to internal/auth/jwt.go index 22b3a3f..c7bc391 100644 --- a/auth/jwt.go +++ b/internal/auth/jwt.go @@ -11,8 +11,8 @@ import ( "os" "time" - "git.a71.su/Andrew71/pye/config" - "git.a71.su/Andrew71/pye/storage" + "git.a71.su/Andrew71/pye/internal/config" + "git.a71.su/Andrew71/pye/internal/models/user" "github.com/golang-jwt/jwt/v5" ) @@ -66,7 +66,7 @@ func ServePublicKey(w http.ResponseWriter, r *http.Request) { } // Create creates a JSON Web Token that expires after a week -func Create(user storage.User) (token string, err error) { +func Create(user user.User) (token string, err error) { t := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ "iss": "pye", diff --git a/config/config.go b/internal/config/config.go similarity index 100% rename from config/config.go rename to internal/config/config.go diff --git a/logging/log.go b/internal/logging/log.go similarity index 95% rename from logging/log.go rename to internal/logging/log.go index 752c4e9..ec62790 100644 --- a/logging/log.go +++ b/internal/logging/log.go @@ -6,7 +6,7 @@ import ( "log/slog" "os" - "git.a71.su/Andrew71/pye/config" + "git.a71.su/Andrew71/pye/internal/config" "github.com/go-chi/chi/middleware" ) diff --git a/storage/user.go b/internal/models/user/user.go similarity index 97% rename from storage/user.go rename to internal/models/user/user.go index 52dc33f..cd18dea 100644 --- a/storage/user.go +++ b/internal/models/user/user.go @@ -1,4 +1,4 @@ -package storage +package user import ( "log/slog" diff --git a/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go similarity index 73% rename from storage/sqlite/sqlite.go rename to internal/storage/sqlite/sqlite.go index e10ce79..da042c3 100644 --- a/storage/sqlite/sqlite.go +++ b/internal/storage/sqlite/sqlite.go @@ -3,11 +3,13 @@ package sqlite import ( "database/sql" "errors" + "fmt" "log/slog" "os" - "git.a71.su/Andrew71/pye/storage" - _ "github.com/mattn/go-sqlite3" + "git.a71.su/Andrew71/pye/internal/models/user" + "git.a71.su/Andrew71/pye/internal/storage" + sqlite "github.com/mattn/go-sqlite3" ) const create string = ` @@ -24,30 +26,36 @@ type SQLiteStorage struct { } func (s SQLiteStorage) Add(email, password string) error { - user, err := storage.New(email, password) + user, err := user.New(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 { + e, ok := err.(sqlite.Error) + if ok && errors.Is(e.ExtendedCode, sqlite.ErrConstraintUnique) { + // Return a standard error if the user already exists + slog.Info("can't add user because email already taken", "user", user) + return fmt.Errorf("%w (%s)", storage.ErrExist, user.Email) + } slog.Error("error adding user to database", "error", err, "user", user) return err } return nil } -func (s SQLiteStorage) ById(uuid string) (storage.User, bool) { +func (s SQLiteStorage) ById(uuid string) (user.User, bool) { row := s.db.QueryRow("select * from users where uuid = $1", uuid) - user := storage.User{} + user := user.User{} err := row.Scan(&user.Uuid, &user.Email, &user.Hash) return user, err == nil } -func (s SQLiteStorage) ByEmail(email string) (storage.User, bool) { +func (s SQLiteStorage) ByEmail(email string) (user.User, bool) { row := s.db.QueryRow("select * from users where email = $1", email) - user := storage.User{} + user := user.User{} err := row.Scan(&user.Uuid, &user.Email, &user.Hash) return user, err == nil diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..d3cbada --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1,21 @@ +package storage + +import ( + "errors" + + "git.a71.su/Andrew71/pye/internal/models/user" +) + +var ErrExist = errors.New("user already exists") + +// Storage is an interface for arbitrary storage +type Storage interface { + Add(email, password string) error // Add inserts a user into data + ById(uuid string) (user.User, bool) // ById retrieves a user by their UUID + ByEmail(email string) (user.User, bool) // ByEmail retrieves a user by their email + Taken(email string) bool // Taken checks whether an email is taken +} + +// Data stores active information for the app. +// It should be populated on startup +var Data Storage diff --git a/main.go b/main.go index 957ff80..fc5cd60 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main -import "git.a71.su/Andrew71/pye/cmd" +import "git.a71.su/Andrew71/pye/internal/app" func main() { - cmd.Execute() + app.Execute() } diff --git a/storage/storage.go b/storage/storage.go deleted file mode 100644 index c49feed..0000000 --- a/storage/storage.go +++ /dev/null @@ -1,13 +0,0 @@ -package storage - -// Storage is an interface for arbitrary storage -type Storage interface { - Add(email, password string) error // Add inserts a user into data - ById(uuid string) (User, bool) // ById retrieves a user by their UUID - ByEmail(email string) (User, bool) // ByEmail retrieves a user by their email - Taken(email string) bool // Taken checks whether an email is taken -} - -// Data stores active information for the app. -// It should be populated on startup -var Data Storage