This commit is contained in:
lbenedar
2026-03-20 17:46:24 +03:00
parent e47eda4cda
commit efb58d7c15
5 changed files with 83 additions and 9 deletions

View File

@@ -56,3 +56,8 @@ func (app *application) rateLimitExceededResponse(w http.ResponseWriter, r *http
message := "rate limit exceeded" message := "rate limit exceeded"
app.errorResponse(w, r, http.StatusTooManyRequests, message) 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)
}

View File

@@ -23,5 +23,7 @@ func (app *application) routes() http.Handler {
router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler) router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler)
router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler) router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler)
router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler)
return app.recoverPanic(app.rateLimit(router)) return app.recoverPanic(app.rateLimit(router))
} }

66
cmd/api/tokens.go Normal file
View File

@@ -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)
}
}

View File

@@ -12,15 +12,16 @@ import (
) )
const ( const (
ScopeActivation = "activation" ScopeActivation = "activation"
ScopeAuthentication = "authentication"
) )
type Token struct { type Token struct {
Plaintext string Plaintext string `json:"token"`
Hash []byte Hash []byte `json:"-"`
UserID int64 UserID int64 `json:"-"`
Expiry time.Time Expiry time.Time `json:"expiry"`
Scope string Scope string `json:"-"`
} }
func generateToken(userID int64, ttl time.Duration, scope string) (*Token, error) { 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 { func (m TokenModel) Insert(token *Token) error {
query := ` query := `
INSERT INTO tokens (hash, user_id, expirt, scope) INSERT INTO tokens (hash, user_id, expiry, scope)
VALUES ($1, $2, $3, $4)` VALUES ($1, $2, $3, $4)`
args := []any{token.Hash, token.UserID, token.Expiry, token.Scope} args := []any{token.Hash, token.UserID, token.Expiry, token.Scope}

View File

@@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS tokens { CREATE TABLE IF NOT EXISTS tokens (
hash bytea PRIMARY KEY, hash bytea PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users ON DELETE CASCADE, user_id bigint NOT NULL REFERENCES users ON DELETE CASCADE,
expiry timestamp(0) with time zone NOT NULL, expiry timestamp(0) with time zone NOT NULL,
scope text NOT NULL scope text NOT NULL
}; );