diff --git a/cmd/api/errors.go b/cmd/api/errors.go index d5c4892..93f6dd0 100644 --- a/cmd/api/errors.go +++ b/cmd/api/errors.go @@ -56,3 +56,8 @@ func (app *application) rateLimitExceededResponse(w http.ResponseWriter, r *http message := "rate limit exceeded" app.errorResponse(w, r, http.StatusTooManyRequests, message) } + +func (app *application) invalidCredentialsResponse(w http.ResponseWriter, r *http.Request) { + message := "invalid authentication credentials" + app.errorResponse(w, r, http.StatusUnauthorized, message) +} diff --git a/cmd/api/routes.go b/cmd/api/routes.go index 3d13c28..4c54f5b 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -23,5 +23,7 @@ func (app *application) routes() http.Handler { router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler) router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler) + router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler) + return app.recoverPanic(app.rateLimit(router)) } diff --git a/cmd/api/tokens.go b/cmd/api/tokens.go new file mode 100644 index 0000000..a96710c --- /dev/null +++ b/cmd/api/tokens.go @@ -0,0 +1,66 @@ +package main + +import ( + "errors" + "net/http" + "time" + + "gitea.local.lab/Lbenedar/greenlight/internal/data" + "gitea.local.lab/Lbenedar/greenlight/internal/validator" +) + +func (app *application) createAuthenticationTokenHandler(w http.ResponseWriter, r *http.Request) { + var input struct { + Email string `json:"email"` + Password string `json:"password"` + } + + err := app.readJSON(w, r, &input) + if err != nil { + app.badRequestResponse(w, r, err) + return + } + + v := validator.New() + + data.ValidateEmail(v, input.Email) + data.ValidatePasswordPlaintext(v, input.Password) + + if !v.Valid() { + app.failedValidationResponse(w, r, v.Errors) + return + } + + user, err := app.models.Users.GetByEmail(input.Email) + if err != nil { + switch { + case errors.Is(err, data.ErrRecordNotFound): + app.invalidCredentialsResponse(w, r) + default: + app.serverErrorResponse(w, r, err) + } + return + } + + match, err := user.Password.Matches(input.Password) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + if !match { + app.invalidCredentialsResponse(w, r) + return + } + + token, err := app.models.Tokens.New(user.ID, 24*time.Hour, data.ScopeAuthentication) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } + + err = app.writeJSON(w, http.StatusCreated, envelope{"authentication_token": token}, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} diff --git a/internal/data/tokens.go b/internal/data/tokens.go index a34cda5..38a1640 100644 --- a/internal/data/tokens.go +++ b/internal/data/tokens.go @@ -12,15 +12,16 @@ import ( ) const ( - ScopeActivation = "activation" + ScopeActivation = "activation" + ScopeAuthentication = "authentication" ) type Token struct { - Plaintext string - Hash []byte - UserID int64 - Expiry time.Time - Scope string + Plaintext string `json:"token"` + Hash []byte `json:"-"` + UserID int64 `json:"-"` + Expiry time.Time `json:"expiry"` + Scope string `json:"-"` } func generateToken(userID int64, ttl time.Duration, scope string) (*Token, error) { @@ -66,7 +67,7 @@ func (m TokenModel) New(userID int64, ttl time.Duration, scope string) (*Token, func (m TokenModel) Insert(token *Token) error { query := ` - INSERT INTO tokens (hash, user_id, expirt, scope) + INSERT INTO tokens (hash, user_id, expiry, scope) VALUES ($1, $2, $3, $4)` args := []any{token.Hash, token.UserID, token.Expiry, token.Scope} diff --git a/migrations/000005_create_tokens_table.up.sql b/migrations/000005_create_tokens_table.up.sql index 4b7c276..4a55baf 100644 --- a/migrations/000005_create_tokens_table.up.sql +++ b/migrations/000005_create_tokens_table.up.sql @@ -1,6 +1,6 @@ -CREATE TABLE IF NOT EXISTS tokens { +CREATE TABLE IF NOT EXISTS tokens ( hash bytea PRIMARY KEY, user_id bigint NOT NULL REFERENCES users ON DELETE CASCADE, expiry timestamp(0) with time zone NOT NULL, scope text NOT NULL -}; \ No newline at end of file +); \ No newline at end of file