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
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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=
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
-- 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
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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_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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user