login
This commit is contained in:
parent
0af44340d5
commit
6bd886664d
@ -12,19 +12,23 @@ import (
|
|||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
"go.uber.org/fx/fxevent"
|
"go.uber.org/fx/fxevent"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fx.New(
|
fx.New(
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
config.Provide, //config
|
config.Provide,
|
||||||
logger.ProvideLogger, //logger
|
logger.ProvideLogger,
|
||||||
db.ProvidePool, // pool provider
|
db.ProvidePool,
|
||||||
usersdb.New, // generated code for sqlc
|
func(pool *pgxpool.Pool) usersdb.Querier {
|
||||||
users.NewService, // service
|
return usersdb.New(pool)
|
||||||
http.NewServer, // http server
|
},
|
||||||
health.NewHandler, // http handler
|
users.NewService,
|
||||||
auth.NewHandler, //http handler
|
http.NewServer,
|
||||||
|
health.NewHandler,
|
||||||
|
auth.NewHandler,
|
||||||
),
|
),
|
||||||
fx.Invoke(
|
fx.Invoke(
|
||||||
http.GlueRoutes,
|
http.GlueRoutes,
|
||||||
|
|||||||
4
go.mod
4
go.mod
@ -3,9 +3,12 @@ module git.kanopo.dev/rhythm/rhythm-backend
|
|||||||
go 1.26.2
|
go 1.26.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.9.2
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/pressly/goose/v3 v3.27.0
|
github.com/pressly/goose/v3 v3.27.0
|
||||||
|
golang.org/x/crypto v0.48.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -34,7 +37,6 @@ require (
|
|||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
go.uber.org/dig v1.19.0 // indirect
|
go.uber.org/dig v1.19.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/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -29,6 +29,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=
|
||||||
|
|||||||
@ -5,12 +5,16 @@ import (
|
|||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppEnv string
|
AppEnv string
|
||||||
DbUrl string
|
DbUrl string
|
||||||
ServerPort string
|
ServerPort string
|
||||||
|
JWTSecret string
|
||||||
|
JWTExpiry time.Duration
|
||||||
|
RefreshExpiry time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() Config {
|
func Load() Config {
|
||||||
@ -22,29 +26,35 @@ func Load() Config {
|
|||||||
port := getEnv("DB_PORT")
|
port := getEnv("DB_PORT")
|
||||||
host := getEnv("DB_HOST")
|
host := getEnv("DB_HOST")
|
||||||
|
|
||||||
// postgres://admin:admin@localhost:5432/admin_db
|
|
||||||
dbUrl = fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=disable", username, password, host, port, name)
|
dbUrl = fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=disable", username, password, host, port, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
appEnv := os.Getenv("APP_ENV") // development | production
|
appEnv := os.Getenv("APP_ENV")
|
||||||
if appEnv == "" {
|
if appEnv == "" {
|
||||||
appEnv = "development"
|
appEnv = "development"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jwtSecret := getEnv("JWT_SECRET")
|
||||||
|
if jwtSecret == "" {
|
||||||
|
log.Fatal("JWT_SECRET env variable is required")
|
||||||
|
}
|
||||||
|
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
AppEnv: appEnv,
|
AppEnv: appEnv,
|
||||||
DbUrl: dbUrl,
|
DbUrl: dbUrl,
|
||||||
ServerPort: "8080",
|
ServerPort: "8080",
|
||||||
|
JWTSecret: jwtSecret,
|
||||||
|
JWTExpiry: time.Minute * 15,
|
||||||
|
RefreshExpiry: time.Hour * 24 * 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnv(key string) string {
|
func getEnv(key string) string {
|
||||||
v := os.Getenv(key)
|
v := os.Getenv(key)
|
||||||
if v == "" {
|
if v == "" {
|
||||||
log.Fatalf("The env variable %v is not defined and the applciation can not operate without\n", key)
|
log.Fatalf("The env variable %v is not defined and the application can not operate without\n", key)
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,15 @@
|
|||||||
package usersdb
|
package usersdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID pgtype.UUID
|
ID uuid.UUID
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
CreatedAt pgtype.Timestamptz
|
CreatedAt time.Time
|
||||||
UpdatedAt pgtype.Timestamptz
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,13 +7,14 @@ package usersdb
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Querier interface {
|
type Querier interface {
|
||||||
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
||||||
DeleteUser(ctx context.Context, id pgtype.UUID) error
|
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||||
GetUser(ctx context.Context, id pgtype.UUID) (User, error)
|
GetUser(ctx context.Context, id uuid.UUID) (User, error)
|
||||||
|
GetUserByEmail(ctx context.Context, email string) (User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Querier = (*Queries)(nil)
|
var _ Querier = (*Queries)(nil)
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
select * from users
|
select * from users
|
||||||
where id = $1 limit 1;
|
where id = $1 limit 1;
|
||||||
|
|
||||||
|
-- name: GetUserByEmail :one
|
||||||
|
select * from users
|
||||||
|
where email = $1 limit 1;
|
||||||
|
|
||||||
-- name: CreateUser :one
|
-- name: CreateUser :one
|
||||||
insert into users (
|
insert into users (
|
||||||
email, password
|
email, password
|
||||||
|
|||||||
@ -8,7 +8,7 @@ package usersdb
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const createUser = `-- name: CreateUser :one
|
const createUser = `-- name: CreateUser :one
|
||||||
@ -43,7 +43,7 @@ DELETE FROM users
|
|||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) DeleteUser(ctx context.Context, id pgtype.UUID) error {
|
func (q *Queries) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||||
_, err := q.db.Exec(ctx, deleteUser, id)
|
_, err := q.db.Exec(ctx, deleteUser, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ select id, email, password, created_at, updated_at from users
|
|||||||
where id = $1 limit 1
|
where id = $1 limit 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (User, error) {
|
func (q *Queries) GetUser(ctx context.Context, id uuid.UUID) (User, error) {
|
||||||
row := q.db.QueryRow(ctx, getUser, id)
|
row := q.db.QueryRow(ctx, getUser, id)
|
||||||
var i User
|
var i User
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@ -65,3 +65,21 @@ func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (User, error) {
|
|||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||||
|
select id, email, password, created_at, updated_at from users
|
||||||
|
where email = $1 limit 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getUserByEmail, email)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.Password,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|||||||
@ -1,48 +1,91 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
|
||||||
|
"git.kanopo.dev/rhythm/rhythm-backend/internal/service/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
|
svc users.Service
|
||||||
log *zap.SugaredLogger
|
log *zap.SugaredLogger
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(log *zap.SugaredLogger) *Handler {
|
func NewHandler(svc users.Service, log *zap.SugaredLogger, cfg *config.Config) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
|
svc: svc,
|
||||||
log: log,
|
log: log,
|
||||||
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) {
|
func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) {
|
||||||
rg.POST("/login", h.Login)
|
rg.POST("/login", h.Login)
|
||||||
|
rg.POST("/register", h.Register)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setRefreshTokenCookie(c *gin.Context, token string) {
|
func setRefreshTokenCookie(c *gin.Context, token string, maxAge int) {
|
||||||
maxAge := time.Hour * 24 * 7
|
|
||||||
c.SetCookie(
|
c.SetCookie(
|
||||||
"refresh_token", // name
|
"refresh_token",
|
||||||
token, // value
|
token,
|
||||||
int(maxAge), // maxAge (seconds, 7 days)
|
maxAge,
|
||||||
"/", // path
|
"/",
|
||||||
"", // domain
|
"",
|
||||||
false, // secure (true in production)
|
false,
|
||||||
true, // httpOnly
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) Login(c *gin.Context) {
|
func (h *Handler) Register(c *gin.Context) {
|
||||||
// var req users.LoginReq
|
var req struct {
|
||||||
// if err := c.ShouldBindJSON(&req); err != nil {
|
Email string `json:"email" binding:"required,email"`
|
||||||
// c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
Password string `json:"password" binding:"required,min=8"`
|
||||||
// return
|
}
|
||||||
// }
|
|
||||||
//
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
// res := h.service.Login(req)
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
// setRefreshTokenCookie(c, res.RefreshToken)
|
return
|
||||||
c.JSON(http.StatusOK, gin.H{"msg": "ok"})
|
}
|
||||||
|
|
||||||
|
result, err := h.svc.Register(c.Request.Context(), req.Email, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
h.log.Error("registration error", "error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setRefreshTokenCookie(c, result.RefreshToken, int(h.cfg.RefreshExpiry))
|
||||||
|
c.JSON(http.StatusCreated, gin.H{"accessToken": result.AccessToken})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Login(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Email string `json:"email" binding:"required,email"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.svc.Login(c.Request.Context(), req.Email, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, users.ErrInvalidCredentials) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.log.Error("login error", "error", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setRefreshTokenCookie(c, result.RefreshToken, int(h.cfg.RefreshExpiry))
|
||||||
|
c.JSON(http.StatusOK, gin.H{"accessToken": result.AccessToken})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,5 +12,4 @@ func GlueRoutes(r *gin.Engine, healthHandler *health.Handler, authHandler *auth.
|
|||||||
|
|
||||||
v1 := api.Group("/v1")
|
v1 := api.Group("/v1")
|
||||||
authHandler.RegisterRoutes(v1.Group("/auth"))
|
authHandler.RegisterRoutes(v1.Group("/auth"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
68
internal/jwt/jwt.go
Normal file
68
internal/jwt/jwt.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenPair struct {
|
||||||
|
AccessToken string
|
||||||
|
RefreshToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateTokenPair(userID string, cfg *config.Config) (TokenPair, error) {
|
||||||
|
accessToken, err := generateToken(userID, cfg.JWTSecret, cfg.JWTExpiry)
|
||||||
|
if err != nil {
|
||||||
|
return TokenPair{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err := generateToken(userID, cfg.JWTSecret, cfg.RefreshExpiry)
|
||||||
|
if err != nil {
|
||||||
|
return TokenPair{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return TokenPair{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateToken(userID, secret string, expiry time.Duration) (string, error) {
|
||||||
|
claims := &Claims{
|
||||||
|
UserID: userID,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiry)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString([]byte(secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateToken(tokenString, secret string) (*Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, errors.New("unexpected signing method")
|
||||||
|
}
|
||||||
|
return []byte(secret), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
}
|
||||||
15
internal/password/password.go
Normal file
15
internal/password/password.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package password
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPassword(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@ -1,34 +1,104 @@
|
|||||||
package users
|
package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
usersdb "git.kanopo.dev/rhythm/rhythm-backend/internal/db/users"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
|
||||||
|
usersdb "git.kanopo.dev/rhythm/rhythm-backend/internal/db/users"
|
||||||
|
"git.kanopo.dev/rhythm/rhythm-backend/internal/jwt"
|
||||||
|
"git.kanopo.dev/rhythm/rhythm-backend/internal/password"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
var ErrInvalidCredentials = errors.New("invalid credentials")
|
||||||
|
|
||||||
|
type AuthResult struct {
|
||||||
|
AccessToken string
|
||||||
|
RefreshToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Login(ctx context.Context, email, password string) (*AuthResult, error)
|
||||||
|
Register(ctx context.Context, email, password string) (*AuthResult, error)
|
||||||
|
GetUserByEmail(ctx context.Context, email string) (usersdb.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
repo usersdb.Querier
|
repo usersdb.Querier
|
||||||
|
cfg *config.Config
|
||||||
log *zap.SugaredLogger
|
log *zap.SugaredLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(repo usersdb.Querier, log *zap.SugaredLogger) *Service {
|
func NewService(repo usersdb.Querier, cfg *config.Config, log *zap.SugaredLogger) Service {
|
||||||
return &Service{
|
return &service{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
cfg: cfg,
|
||||||
log: log,
|
log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// type LoginReq struct {
|
func (s *service) Login(ctx context.Context, email, passwordPlain string) (*AuthResult, error) {
|
||||||
// Email string `json:"email" binding:"required"`
|
user, err := s.repo.GetUserByEmail(ctx, email)
|
||||||
// Password string `json:"password" binding:"required"`
|
if err != nil {
|
||||||
// }
|
s.log.Infof("login failed: user not found with email %v", email)
|
||||||
// type AuthRes struct {
|
return nil, ErrInvalidCredentials
|
||||||
// AccessToken string `json:"accessToken"`
|
}
|
||||||
// RefreshToken string // not parset to json, set with cookies
|
|
||||||
// }
|
if !password.CheckPassword(passwordPlain, user.Password) {
|
||||||
//
|
s.log.Infof("login failed: invalid password for email %v", email)
|
||||||
// func (s *Service) Login(req LoginReq) AuthRes {
|
return nil, ErrInvalidCredentials
|
||||||
// return AuthRes{
|
}
|
||||||
// AccessToken: "ciao",
|
|
||||||
// RefreshToken: "ciao",
|
tokenPair, err := jwt.GenerateTokenPair(user.ID.String(), s.cfg)
|
||||||
// }
|
if err != nil {
|
||||||
// }
|
s.log.Errorf("failed to generate token pair %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Infof("user logged in successfully with email %v", email)
|
||||||
|
|
||||||
|
return &AuthResult{
|
||||||
|
AccessToken: tokenPair.AccessToken,
|
||||||
|
RefreshToken: tokenPair.RefreshToken,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Register(ctx context.Context, email, passwordPlain string) (*AuthResult, error) {
|
||||||
|
_, err := s.repo.GetUserByEmail(ctx, email)
|
||||||
|
if err == nil {
|
||||||
|
s.log.Infof("registration failed: email already exists %v", email)
|
||||||
|
return nil, ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := password.HashPassword(passwordPlain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.repo.CreateUser(ctx, usersdb.CreateUserParams{
|
||||||
|
Email: email,
|
||||||
|
Password: hash,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenPair, err := jwt.GenerateTokenPair(user.ID.String(), s.cfg)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("failed to generate token pair", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Info("user registered successfully", "email", email)
|
||||||
|
|
||||||
|
return &AuthResult{
|
||||||
|
AccessToken: tokenPair.AccessToken,
|
||||||
|
RefreshToken: tokenPair.RefreshToken,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetUserByEmail(ctx context.Context, email string) (usersdb.User, error) {
|
||||||
|
return s.repo.GetUserByEmail(ctx, email)
|
||||||
|
}
|
||||||
|
|||||||
@ -15,6 +15,15 @@ sql:
|
|||||||
sql_package: "pgx/v5"
|
sql_package: "pgx/v5"
|
||||||
emit_json_tags: false
|
emit_json_tags: false
|
||||||
emit_interface: true
|
emit_interface: true
|
||||||
|
overrides:
|
||||||
|
- db_type: "uuid"
|
||||||
|
go_type:
|
||||||
|
import: "github.com/google/uuid"
|
||||||
|
type: "UUID"
|
||||||
|
- db_type: "timestamptz"
|
||||||
|
go_type:
|
||||||
|
import: "time"
|
||||||
|
type: "Time"
|
||||||
overrides:
|
overrides:
|
||||||
go: null
|
go: null
|
||||||
plugins: []
|
plugins: []
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user