implement JWT tokens, regenerate docs and sqlc
This commit is contained in:
@@ -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 <token>`
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
# Postgres
|
||||
DATABASE_URL=postgresql://user:password@host:5432/database
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=
|
||||
JWT_ISSUER=
|
||||
JWT_TTL_MINUTES=
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -2,7 +2,9 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
db "zardzul/music-index/sqlc"
|
||||
"zardzul/music-index/utils"
|
||||
|
||||
"zardzul/music-index/repository"
|
||||
|
||||
@@ -14,10 +16,18 @@ import (
|
||||
|
||||
type UserHandler struct {
|
||||
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})
|
||||
}
|
||||
|
||||
+24
-2
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
-- name: GetUserAuthByEmail :one
|
||||
SELECT id, user_name, user_mail, password
|
||||
FROM users
|
||||
WHERE user_mail = $1;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+10
-2
@@ -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)
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ type User struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
type UserAlbum struct {
|
||||
|
||||
+1
-3
@@ -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)
|
||||
|
||||
+25
-48
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user