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"
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.MethodPut, "/v1/users/activated", app.activateUserHandler)
router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler)
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 (
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}

View File

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