From d5e608feeb54f2482a74c3fb5d926c340c39a126 Mon Sep 17 00:00:00 2001 From: zardzul Date: Sat, 14 Mar 2026 18:46:48 +0100 Subject: [PATCH] implement JWT tokens, regenerate docs and sqlc --- README.md | 13 ++++++ api/.env.example | 7 ++- api/docs/docs.go | 74 +++++++++++++++++++++++++++++++ api/docs/swagger.json | 74 +++++++++++++++++++++++++++++++ api/docs/swagger.yaml | 48 ++++++++++++++++++++ api/go.mod | 5 ++- api/go.sum | 2 + api/handlers/user_handler.go | 70 +++++++++++++++++++++++++++-- api/main.go | 26 ++++++++++- api/middleware/jwt_auth.go | 31 +++++++++++++ api/queries/users.sql | 15 ++----- api/repository/user_repository.go | 5 +++ api/routes/router.go | 12 ++++- api/sqlc/models.go | 12 +++-- api/sqlc/querier.go | 4 +- api/sqlc/users.sql.go | 73 +++++++++++------------------- api/utils/jwt.go | 48 ++++++++++++++++++++ db/db.sql | 4 +- 18 files changed, 441 insertions(+), 82 deletions(-) create mode 100644 api/middleware/jwt_auth.go create mode 100644 api/utils/jwt.go diff --git a/README.md b/README.md index 9762813..8d2b97e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,19 @@ Music collection software for indexing bought (physical) albums. Build with Golang and Postgres +## Authentication +JWT authentication is enabled for protected endpoints. + +Set these environment variables for the API: + +- `JWT_SECRET` (required) +- `JWT_ISSUER` (optional, defaults to `music-index-api`) +- `JWT_TTL_MINUTES` (optional, defaults to `60`) + +Get a token via `POST /api/v1/users/login` and include it as: + +`Authorization: Bearer ` + ## Swagger The API documentation is available at [/api/v1/swagger/index.html](http://localhost:8080/api/v1/swagger/index.html) after running the application. It provides details about the available endpoints, request/response formats, and other relevant information for developers to interact with the API effectively. diff --git a/api/.env.example b/api/.env.example index 0e8ac2b..08ff93c 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,2 +1,7 @@ # Postgres -DATABASE_URL=postgresql://user:password@host:5432/database \ No newline at end of file +DATABASE_URL=postgresql://user:password@host:5432/database + +# JWT +JWT_SECRET= +JWT_ISSUER= +JWT_TTL_MINUTES= \ No newline at end of file diff --git a/api/docs/docs.go b/api/docs/docs.go index d4d8870..f07f8af 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -89,6 +89,57 @@ const docTemplate = `{ } } }, + "/users/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Log in with email and password", + "parameters": [ + { + "description": "Login payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.ErrorResponse" + } + } + } + } + }, "/users/{id}": { "get": { "produces": [ @@ -170,6 +221,29 @@ const docTemplate = `{ } } }, + "handlers.LoginRequest": { + "type": "object", + "required": [ + "password", + "user_mail" + ], + "properties": { + "password": { + "type": "string" + }, + "user_mail": { + "type": "string" + } + } + }, + "handlers.LoginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, "handlers.UsernameResponse": { "type": "object", "properties": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 0989513..40c54f6 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -82,6 +82,57 @@ } } }, + "/users/login": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Log in with email and password", + "parameters": [ + { + "description": "Login payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.ErrorResponse" + } + } + } + } + }, "/users/{id}": { "get": { "produces": [ @@ -163,6 +214,29 @@ } } }, + "handlers.LoginRequest": { + "type": "object", + "required": [ + "password", + "user_mail" + ], + "properties": { + "password": { + "type": "string" + }, + "user_mail": { + "type": "string" + } + } + }, + "handlers.LoginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, "handlers.UsernameResponse": { "type": "object", "properties": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 7383ac0..cb1f1d9 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -26,6 +26,21 @@ definitions: error: type: string type: object + handlers.LoginRequest: + properties: + password: + type: string + user_mail: + type: string + required: + - password + - user_mail + type: object + handlers.LoginResponse: + properties: + token: + type: string + type: object handlers.UsernameResponse: properties: user_name: @@ -111,4 +126,37 @@ paths: summary: Create a user tags: - user + /users/login: + post: + consumes: + - application/json + parameters: + - description: Login payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/handlers.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.LoginResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.ErrorResponse' + summary: Log in with email and password + tags: + - user swagger: "2.0" diff --git a/api/go.mod b/api/go.mod index 9f6929d..585fe6f 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,11 +4,14 @@ go 1.25.7 require ( github.com/gin-gonic/gin v1.12.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.8.0 + github.com/joho/godotenv v1.5.1 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.4 + golang.org/x/crypto v0.48.0 ) require ( @@ -33,7 +36,6 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect @@ -49,7 +51,6 @@ require ( github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect diff --git a/api/go.sum b/api/go.sum index f74cb14..9463fa7 100644 --- a/api/go.sum +++ b/api/go.sum @@ -46,6 +46,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/api/handlers/user_handler.go b/api/handlers/user_handler.go index 0003e67..d025d7a 100644 --- a/api/handlers/user_handler.go +++ b/api/handlers/user_handler.go @@ -2,7 +2,9 @@ package handlers import ( "net/http" + "time" db "zardzul/music-index/sqlc" + "zardzul/music-index/utils" "zardzul/music-index/repository" @@ -13,11 +15,19 @@ import ( ) type UserHandler struct { - repo repository.UserRepository + repo repository.UserRepository + jwtSecret string + jwtIssuer string + jwtTTL time.Duration } -func NewUserHandler(repo repository.UserRepository) *UserHandler { - return &UserHandler{repo: repo} +func NewUserHandler(repo repository.UserRepository, jwtSecret string, jwtIssuer string, jwtTTL time.Duration) *UserHandler { + return &UserHandler{ + repo: repo, + jwtSecret: jwtSecret, + jwtIssuer: jwtIssuer, + jwtTTL: jwtTTL, + } } type CreateUserRequest struct { @@ -116,3 +126,57 @@ func (h *UserHandler) GetUsernameByID(c *gin.Context) { "user_name": username, }) } + +type LoginRequest struct { + UserMail string `json:"user_mail" binding:"required,email"` + Password string `json:"password" binding:"required"` +} + +type LoginResponse struct { + Token string `json:"token"` +} + +// Login godoc +// @Summary Log in with email and password +// @Tags user +// @Accept json +// @Produce json +// @Param payload body LoginRequest true "Login payload" +// @Success 200 {object} LoginResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /users/login [post] +func (h *UserHandler) Login(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) + return + } + + user, err := h.repo.GetUserAuthByEmail(c.Request.Context(), req.UserMail) + if err != nil { + c.JSON(http.StatusUnauthorized, ErrorResponse{Error: "invalid credentials"}) + return + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { + c.JSON(http.StatusUnauthorized, ErrorResponse{Error: "invalid credentials"}) + return + } + + token, err := utils.GenerateToken( + h.jwtSecret, + h.jwtIssuer, + user.ID.String(), + user.UserName, + user.UserMail, + h.jwtTTL, + ) + if err != nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "failed to create token"}) + return + } + + c.JSON(http.StatusOK, LoginResponse{Token: token}) +} diff --git a/api/main.go b/api/main.go index 2ab94df..861e5f0 100644 --- a/api/main.go +++ b/api/main.go @@ -1,6 +1,9 @@ package main import ( + "os" + "strconv" + "time" "zardzul/music-index/database" _ "zardzul/music-index/docs" "zardzul/music-index/handlers" @@ -28,13 +31,32 @@ func main() { } defer pool.Close() + jwtSecret := os.Getenv("JWT_SECRET") + if jwtSecret == "" { + panic("JWT_SECRET is required") + } + + jwtIssuer := os.Getenv("JWT_ISSUER") + if jwtIssuer == "" { + jwtIssuer = "music-index-api" + } + + jwtTTLMinutes := 60 + if envTTL := os.Getenv("JWT_TTL_MINUTES"); envTTL != "" { + parsed, parseErr := strconv.Atoi(envTTL) + if parseErr != nil || parsed <= 0 { + panic("JWT_TTL_MINUTES must be a positive integer") + } + jwtTTLMinutes = parsed + } + userRepo := repository.NewUserRepository(queries) - userHandler := handlers.NewUserHandler(userRepo) + userHandler := handlers.NewUserHandler(userRepo, jwtSecret, jwtIssuer, time.Duration(jwtTTLMinutes)*time.Minute) artistRepo := repository.NewArtistRepository(queries) artistHandler := handlers.NewArtistHandler(artistRepo) router := gin.Default() - routes.Routes(router, userHandler, artistHandler) + routes.Routes(router, userHandler, artistHandler, jwtSecret) if routerError := router.Run(":8080"); routerError != nil { panic(routerError) diff --git a/api/middleware/jwt_auth.go b/api/middleware/jwt_auth.go new file mode 100644 index 0000000..2c64d84 --- /dev/null +++ b/api/middleware/jwt_auth.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "net/http" + "strings" + + "zardzul/music-index/utils" + + "github.com/gin-gonic/gin" +) + +func JWTAuth(secret string) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing or invalid authorization header"}) + return + } + + claims, err := utils.ValidateToken(secret, parts[1]) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + + c.Set("user_id", claims.Subject) + c.Set("user_name", claims.UserName) + c.Next() + } +} diff --git a/api/queries/users.sql b/api/queries/users.sql index bb8a562..36bba69 100644 --- a/api/queries/users.sql +++ b/api/queries/users.sql @@ -8,17 +8,10 @@ UPDATE users SET user_name = $2, user_mail = $3, password = $4 WHERE id = $1; -- name: GetUsernameByID :one SELECT user_name FROM users WHERE id = $1; --- name: CheckUserExistsByEmail :one -SELECT id FROM users WHERE user_mail = $1; - -- name: GetUserByID :one SELECT id, user_name, user_mail, created_at FROM users WHERE id = $1; --- name: LoginUser :one -SELECT id, user_name, user_mail FROM users WHERE user_mail = $1 AND password = $2; - --- name: UpdateUserSession :exec -UPDATE users SET session_token = $2, session_expiry = $3 WHERE id = $1; - --- name: logoutUser :exec --- This is a placeholder for logout functionality, which typically involves token invalidation or session management rather \ No newline at end of file +-- name: GetUserAuthByEmail :one +SELECT id, user_name, user_mail, password +FROM users +WHERE user_mail = $1; diff --git a/api/repository/user_repository.go b/api/repository/user_repository.go index 55007d2..f5fdf9c 100644 --- a/api/repository/user_repository.go +++ b/api/repository/user_repository.go @@ -10,6 +10,7 @@ import ( type UserRepository interface { CreateUser(ctx context.Context, arg db.CreateUserParams) (pgtype.UUID, error) GetUsernameByID(ctx context.Context, id pgtype.UUID) (string, error) + GetUserAuthByEmail(ctx context.Context, mail string) (db.GetUserAuthByEmailRow, error) } type SQLCUserRepository struct { @@ -27,3 +28,7 @@ func (r *SQLCUserRepository) CreateUser(ctx context.Context, arg db.CreateUserPa func (r *SQLCUserRepository) GetUsernameByID(ctx context.Context, id pgtype.UUID) (string, error) { return r.q.GetUsernameByID(ctx, id) } + +func (r *SQLCUserRepository) GetUserAuthByEmail(ctx context.Context, email string) (db.GetUserAuthByEmailRow, error) { + return r.q.GetUserAuthByEmail(ctx, email) +} diff --git a/api/routes/router.go b/api/routes/router.go index e486723..987cf8e 100644 --- a/api/routes/router.go +++ b/api/routes/router.go @@ -3,13 +3,14 @@ package routes import ( "net/http" "zardzul/music-index/handlers" + "zardzul/music-index/middleware" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) -func Routes(router *gin.Engine, userHandler *handlers.UserHandler, artistHandler *handlers.ArtistHandler) { +func Routes(router *gin.Engine, userHandler *handlers.UserHandler, artistHandler *handlers.ArtistHandler, jwtSecret string) { root := router.Group("/api/v1") { root.GET("/ping", func(c *gin.Context) { @@ -21,10 +22,17 @@ func Routes(router *gin.Engine, userHandler *handlers.UserHandler, artistHandler user := root.Group("/users") { user.POST("/create", userHandler.CreateUser) - user.GET("/:id", userHandler.GetUsernameByID) + user.POST("/login", userHandler.Login) + } + + protectedUser := root.Group("/users") + protectedUser.Use(middleware.JWTAuth(jwtSecret)) + { + protectedUser.GET("/:id", userHandler.GetUsernameByID) } artist := root.Group("/artists") + artist.Use(middleware.JWTAuth(jwtSecret)) { artist.GET("/", artistHandler.GetAll) } diff --git a/api/sqlc/models.go b/api/sqlc/models.go index 741d32f..ce9c96c 100644 --- a/api/sqlc/models.go +++ b/api/sqlc/models.go @@ -25,13 +25,11 @@ type Artist struct { } type User struct { - ID pgtype.UUID `json:"id"` - UserName string `json:"user_name"` - UserMail string `json:"user_mail"` - Password string `json:"password"` - CreatedAt pgtype.Timestamp `json:"created_at"` - SessionToken pgtype.Text `json:"session_token"` - SessionExpiry pgtype.Timestamp `json:"session_expiry"` + ID pgtype.UUID `json:"id"` + UserName string `json:"user_name"` + UserMail string `json:"user_mail"` + Password string `json:"password"` + CreatedAt pgtype.Timestamp `json:"created_at"` } type UserAlbum struct { diff --git a/api/sqlc/querier.go b/api/sqlc/querier.go index 393a0a4..6d1440a 100644 --- a/api/sqlc/querier.go +++ b/api/sqlc/querier.go @@ -12,7 +12,6 @@ import ( type Querier interface { AddUserAlbum(ctx context.Context, arg AddUserAlbumParams) error - CheckUserExistsByEmail(ctx context.Context, userMail string) (pgtype.UUID, error) CreateAlbum(ctx context.Context, arg CreateAlbumParams) (pgtype.UUID, error) CreateArtist(ctx context.Context, arg CreateArtistParams) (pgtype.UUID, error) // users.sql @@ -29,9 +28,9 @@ type Querier interface { GetUserAlbum(ctx context.Context, arg GetUserAlbumParams) (GetUserAlbumRow, error) // user_albums.sql GetUserAlbums(ctx context.Context, userID pgtype.UUID) ([]GetUserAlbumsRow, error) + GetUserAuthByEmail(ctx context.Context, userMail string) (GetUserAuthByEmailRow, error) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDRow, error) GetUsernameByID(ctx context.Context, id pgtype.UUID) (string, error) - LoginUser(ctx context.Context, arg LoginUserParams) (LoginUserRow, error) RemoveUserAlbum(ctx context.Context, arg RemoveUserAlbumParams) error SearchAlbums(ctx context.Context, dollar_1 pgtype.Text) ([]Album, error) SearchArtists(ctx context.Context, dollar_1 pgtype.Text) ([]Artist, error) @@ -39,7 +38,6 @@ type Querier interface { UpdateArtist(ctx context.Context, arg UpdateArtistParams) error UpdateUser(ctx context.Context, arg UpdateUserParams) error UpdateUserAlbumStatus(ctx context.Context, arg UpdateUserAlbumStatusParams) error - UpdateUserSession(ctx context.Context, arg UpdateUserSessionParams) error } var _ Querier = (*Queries)(nil) diff --git a/api/sqlc/users.sql.go b/api/sqlc/users.sql.go index bdef862..02118dc 100644 --- a/api/sqlc/users.sql.go +++ b/api/sqlc/users.sql.go @@ -11,17 +11,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const checkUserExistsByEmail = `-- name: CheckUserExistsByEmail :one -SELECT id FROM users WHERE user_mail = $1 -` - -func (q *Queries) CheckUserExistsByEmail(ctx context.Context, userMail string) (pgtype.UUID, error) { - row := q.db.QueryRow(ctx, checkUserExistsByEmail, userMail) - var id pgtype.UUID - err := row.Scan(&id) - return id, err -} - const createUser = `-- name: CreateUser :one INSERT INTO users (id, user_name, user_mail, password) VALUES ($1, $2, $3, $4) RETURNING id ` @@ -46,6 +35,31 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (pgtype. return id, err } +const getUserAuthByEmail = `-- name: GetUserAuthByEmail :one +SELECT id, user_name, user_mail, password +FROM users +WHERE user_mail = $1 +` + +type GetUserAuthByEmailRow struct { + ID pgtype.UUID `json:"id"` + UserName string `json:"user_name"` + UserMail string `json:"user_mail"` + Password string `json:"password"` +} + +func (q *Queries) GetUserAuthByEmail(ctx context.Context, userMail string) (GetUserAuthByEmailRow, error) { + row := q.db.QueryRow(ctx, getUserAuthByEmail, userMail) + var i GetUserAuthByEmailRow + err := row.Scan( + &i.ID, + &i.UserName, + &i.UserMail, + &i.Password, + ) + return i, err +} + const getUserByID = `-- name: GetUserByID :one SELECT id, user_name, user_mail, created_at FROM users WHERE id = $1 ` @@ -80,28 +94,6 @@ func (q *Queries) GetUsernameByID(ctx context.Context, id pgtype.UUID) (string, return user_name, err } -const loginUser = `-- name: LoginUser :one -SELECT id, user_name, user_mail FROM users WHERE user_mail = $1 AND password = $2 -` - -type LoginUserParams struct { - UserMail string `json:"user_mail"` - Password string `json:"password"` -} - -type LoginUserRow struct { - ID pgtype.UUID `json:"id"` - UserName string `json:"user_name"` - UserMail string `json:"user_mail"` -} - -func (q *Queries) LoginUser(ctx context.Context, arg LoginUserParams) (LoginUserRow, error) { - row := q.db.QueryRow(ctx, loginUser, arg.UserMail, arg.Password) - var i LoginUserRow - err := row.Scan(&i.ID, &i.UserName, &i.UserMail) - return i, err -} - const updateUser = `-- name: UpdateUser :exec UPDATE users SET user_name = $2, user_mail = $3, password = $4 WHERE id = $1 ` @@ -122,18 +114,3 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { ) return err } - -const updateUserSession = `-- name: UpdateUserSession :exec -UPDATE users SET session_token = $2, session_expiry = $3 WHERE id = $1 -` - -type UpdateUserSessionParams struct { - ID pgtype.UUID `json:"id"` - SessionToken pgtype.Text `json:"session_token"` - SessionExpiry pgtype.Timestamp `json:"session_expiry"` -} - -func (q *Queries) UpdateUserSession(ctx context.Context, arg UpdateUserSessionParams) error { - _, err := q.db.Exec(ctx, updateUserSession, arg.ID, arg.SessionToken, arg.SessionExpiry) - return err -} diff --git a/api/utils/jwt.go b/api/utils/jwt.go new file mode 100644 index 0000000..2518314 --- /dev/null +++ b/api/utils/jwt.go @@ -0,0 +1,48 @@ +package utils + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + UserName string `json:"user_name"` + UserMail string `json:"user_mail"` + jwt.RegisteredClaims +} + +func GenerateToken(secret, issuer, userID, userName, userMail string, ttl time.Duration) (string, error) { + now := time.Now() + claims := Claims{ + UserName: userName, + UserMail: userMail, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID, + Issuer: issuer, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(ttl)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(secret)) +} + +func ValidateToken(secret string, tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(t *jwt.Token) (interface{}, error) { + if t.Method != jwt.SigningMethodHS256 { + return nil, errors.New("unexpected signing method") + } + return []byte(secret), nil + }) + if err != nil { + return nil, err + } + + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, errors.New("invalid token") + } + return claims, nil +} diff --git a/db/db.sql b/db/db.sql index aa22a38..ee5110f 100644 --- a/db/db.sql +++ b/db/db.sql @@ -3,9 +3,7 @@ CREATE TABLE IF NOT EXISTS users ( user_name VARCHAR(255) NOT NULL, user_mail VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - session_token VARCHAR(255), - session_expiry TIMESTAMP + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS albums (