implement JWT tokens, regenerate docs and sqlc

This commit is contained in:
zardzul
2026-03-14 18:46:48 +01:00
parent 131aee8638
commit d5e608feeb
18 changed files with 441 additions and 82 deletions
+13
View File
@@ -2,6 +2,19 @@
Music collection software for indexing bought (physical) albums. Build with Golang and Postgres 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 <token>`
## Swagger ## 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. 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.
+6 -1
View File
@@ -1,2 +1,7 @@
# Postgres # Postgres
DATABASE_URL=postgresql://user:password@host:5432/database DATABASE_URL=postgresql://user:password@host:5432/database
# JWT
JWT_SECRET=
JWT_ISSUER=
JWT_TTL_MINUTES=
+74
View File
@@ -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}": { "/users/{id}": {
"get": { "get": {
"produces": [ "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": { "handlers.UsernameResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
+74
View File
@@ -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}": { "/users/{id}": {
"get": { "get": {
"produces": [ "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": { "handlers.UsernameResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
+48
View File
@@ -26,6 +26,21 @@ definitions:
error: error:
type: string type: string
type: object 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: handlers.UsernameResponse:
properties: properties:
user_name: user_name:
@@ -111,4 +126,37 @@ paths:
summary: Create a user summary: Create a user
tags: tags:
- user - 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" swagger: "2.0"
+3 -2
View File
@@ -4,11 +4,14 @@ go 1.25.7
require ( require (
github.com/gin-gonic/gin v1.12.0 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/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.8.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/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.48.0
) )
require ( require (
@@ -33,7 +36,6 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // 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/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
@@ -49,7 +51,6 @@ require (
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.22.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/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
+2
View File
@@ -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-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 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+67 -3
View File
@@ -2,7 +2,9 @@ package handlers
import ( import (
"net/http" "net/http"
"time"
db "zardzul/music-index/sqlc" db "zardzul/music-index/sqlc"
"zardzul/music-index/utils"
"zardzul/music-index/repository" "zardzul/music-index/repository"
@@ -13,11 +15,19 @@ import (
) )
type UserHandler struct { type UserHandler struct {
repo repository.UserRepository repo repository.UserRepository
jwtSecret string
jwtIssuer string
jwtTTL time.Duration
} }
func NewUserHandler(repo repository.UserRepository) *UserHandler { func NewUserHandler(repo repository.UserRepository, jwtSecret string, jwtIssuer string, jwtTTL time.Duration) *UserHandler {
return &UserHandler{repo: repo} return &UserHandler{
repo: repo,
jwtSecret: jwtSecret,
jwtIssuer: jwtIssuer,
jwtTTL: jwtTTL,
}
} }
type CreateUserRequest struct { type CreateUserRequest struct {
@@ -116,3 +126,57 @@ func (h *UserHandler) GetUsernameByID(c *gin.Context) {
"user_name": username, "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})
}
+24 -2
View File
@@ -1,6 +1,9 @@
package main package main
import ( import (
"os"
"strconv"
"time"
"zardzul/music-index/database" "zardzul/music-index/database"
_ "zardzul/music-index/docs" _ "zardzul/music-index/docs"
"zardzul/music-index/handlers" "zardzul/music-index/handlers"
@@ -28,13 +31,32 @@ func main() {
} }
defer pool.Close() 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) userRepo := repository.NewUserRepository(queries)
userHandler := handlers.NewUserHandler(userRepo) userHandler := handlers.NewUserHandler(userRepo, jwtSecret, jwtIssuer, time.Duration(jwtTTLMinutes)*time.Minute)
artistRepo := repository.NewArtistRepository(queries) artistRepo := repository.NewArtistRepository(queries)
artistHandler := handlers.NewArtistHandler(artistRepo) artistHandler := handlers.NewArtistHandler(artistRepo)
router := gin.Default() router := gin.Default()
routes.Routes(router, userHandler, artistHandler) routes.Routes(router, userHandler, artistHandler, jwtSecret)
if routerError := router.Run(":8080"); routerError != nil { if routerError := router.Run(":8080"); routerError != nil {
panic(routerError) panic(routerError)
+31
View File
@@ -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()
}
}
+4 -11
View File
@@ -8,17 +8,10 @@ UPDATE users SET user_name = $2, user_mail = $3, password = $4 WHERE id = $1;
-- name: GetUsernameByID :one -- name: GetUsernameByID :one
SELECT user_name FROM users WHERE id = $1; SELECT user_name FROM users WHERE id = $1;
-- name: CheckUserExistsByEmail :one
SELECT id FROM users WHERE user_mail = $1;
-- name: GetUserByID :one -- name: GetUserByID :one
SELECT id, user_name, user_mail, created_at FROM users WHERE id = $1; SELECT id, user_name, user_mail, created_at FROM users WHERE id = $1;
-- name: LoginUser :one -- name: GetUserAuthByEmail :one
SELECT id, user_name, user_mail FROM users WHERE user_mail = $1 AND password = $2; SELECT id, user_name, user_mail, password
FROM users
-- name: UpdateUserSession :exec WHERE user_mail = $1;
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
+5
View File
@@ -10,6 +10,7 @@ import (
type UserRepository interface { type UserRepository interface {
CreateUser(ctx context.Context, arg db.CreateUserParams) (pgtype.UUID, error) CreateUser(ctx context.Context, arg db.CreateUserParams) (pgtype.UUID, error)
GetUsernameByID(ctx context.Context, id pgtype.UUID) (string, error) GetUsernameByID(ctx context.Context, id pgtype.UUID) (string, error)
GetUserAuthByEmail(ctx context.Context, mail string) (db.GetUserAuthByEmailRow, error)
} }
type SQLCUserRepository struct { 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) { func (r *SQLCUserRepository) GetUsernameByID(ctx context.Context, id pgtype.UUID) (string, error) {
return r.q.GetUsernameByID(ctx, id) return r.q.GetUsernameByID(ctx, id)
} }
func (r *SQLCUserRepository) GetUserAuthByEmail(ctx context.Context, email string) (db.GetUserAuthByEmailRow, error) {
return r.q.GetUserAuthByEmail(ctx, email)
}
+10 -2
View File
@@ -3,13 +3,14 @@ package routes
import ( import (
"net/http" "net/http"
"zardzul/music-index/handlers" "zardzul/music-index/handlers"
"zardzul/music-index/middleware"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files" swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" 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 := router.Group("/api/v1")
{ {
root.GET("/ping", func(c *gin.Context) { 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 := root.Group("/users")
{ {
user.POST("/create", userHandler.CreateUser) 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 := root.Group("/artists")
artist.Use(middleware.JWTAuth(jwtSecret))
{ {
artist.GET("/", artistHandler.GetAll) artist.GET("/", artistHandler.GetAll)
} }
+5 -7
View File
@@ -25,13 +25,11 @@ type Artist struct {
} }
type User struct { type User struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
UserName string `json:"user_name"` UserName string `json:"user_name"`
UserMail string `json:"user_mail"` UserMail string `json:"user_mail"`
Password string `json:"password"` Password string `json:"password"`
CreatedAt pgtype.Timestamp `json:"created_at"` CreatedAt pgtype.Timestamp `json:"created_at"`
SessionToken pgtype.Text `json:"session_token"`
SessionExpiry pgtype.Timestamp `json:"session_expiry"`
} }
type UserAlbum struct { type UserAlbum struct {
+1 -3
View File
@@ -12,7 +12,6 @@ import (
type Querier interface { type Querier interface {
AddUserAlbum(ctx context.Context, arg AddUserAlbumParams) error 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) CreateAlbum(ctx context.Context, arg CreateAlbumParams) (pgtype.UUID, error)
CreateArtist(ctx context.Context, arg CreateArtistParams) (pgtype.UUID, error) CreateArtist(ctx context.Context, arg CreateArtistParams) (pgtype.UUID, error)
// users.sql // users.sql
@@ -29,9 +28,9 @@ type Querier interface {
GetUserAlbum(ctx context.Context, arg GetUserAlbumParams) (GetUserAlbumRow, error) GetUserAlbum(ctx context.Context, arg GetUserAlbumParams) (GetUserAlbumRow, error)
// user_albums.sql // user_albums.sql
GetUserAlbums(ctx context.Context, userID pgtype.UUID) ([]GetUserAlbumsRow, error) 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) GetUserByID(ctx context.Context, id pgtype.UUID) (GetUserByIDRow, error)
GetUsernameByID(ctx context.Context, id pgtype.UUID) (string, 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 RemoveUserAlbum(ctx context.Context, arg RemoveUserAlbumParams) error
SearchAlbums(ctx context.Context, dollar_1 pgtype.Text) ([]Album, error) SearchAlbums(ctx context.Context, dollar_1 pgtype.Text) ([]Album, error)
SearchArtists(ctx context.Context, dollar_1 pgtype.Text) ([]Artist, 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 UpdateArtist(ctx context.Context, arg UpdateArtistParams) error
UpdateUser(ctx context.Context, arg UpdateUserParams) error UpdateUser(ctx context.Context, arg UpdateUserParams) error
UpdateUserAlbumStatus(ctx context.Context, arg UpdateUserAlbumStatusParams) error UpdateUserAlbumStatus(ctx context.Context, arg UpdateUserAlbumStatusParams) error
UpdateUserSession(ctx context.Context, arg UpdateUserSessionParams) error
} }
var _ Querier = (*Queries)(nil) var _ Querier = (*Queries)(nil)
+25 -48
View File
@@ -11,17 +11,6 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 const createUser = `-- name: CreateUser :one
INSERT INTO users (id, user_name, user_mail, password) VALUES ($1, $2, $3, $4) RETURNING id 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 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 const getUserByID = `-- name: GetUserByID :one
SELECT id, user_name, user_mail, created_at FROM users WHERE id = $1 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 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 const updateUser = `-- name: UpdateUser :exec
UPDATE users SET user_name = $2, user_mail = $3, password = $4 WHERE id = $1 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 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
}
+48
View File
@@ -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
}
+1 -3
View File
@@ -3,9 +3,7 @@ CREATE TABLE IF NOT EXISTS users (
user_name VARCHAR(255) NOT NULL, user_name VARCHAR(255) NOT NULL,
user_mail VARCHAR(255) NOT NULL UNIQUE, user_mail VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
session_token VARCHAR(255),
session_expiry TIMESTAMP
); );
CREATE TABLE IF NOT EXISTS albums ( CREATE TABLE IF NOT EXISTS albums (