Initial fullstack project setup with Next.js 15, Gin, PostgreSQL and Docker Compose
- Frontend: Next.js 15 (App Router), Auth.js v5, shadcn/ui, MagicUI - Backend: Go + Gin + GORM with layered architecture - Auth: Local credentials login with optional Keycloak OAuth binding - Admin: RBAC user management for admin role - Dev: Docker Compose with hot reload for both frontend and backend - Docker: 3-service orchestration (frontend, backend, postgres) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
38
backend/internal/config/config.go
Normal file
38
backend/internal/config/config.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DatabaseURL string
|
||||
ServerPort string
|
||||
ServerAPIURL string
|
||||
AuthSecret string
|
||||
KeycloakIssuer string
|
||||
KeycloakID string
|
||||
KeycloakSecret string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
_ = godotenv.Load()
|
||||
|
||||
return &Config{
|
||||
DatabaseURL: getEnv("DATABASE_URL", "postgres://evan:evanpass@localhost:5432/evanpage?sslmode=disable"),
|
||||
ServerPort: getEnv("SERVER_PORT", "8080"),
|
||||
ServerAPIURL: getEnv("SERVER_API_URL", "http://localhost:8080"),
|
||||
AuthSecret: getEnv("AUTH_SECRET", ""),
|
||||
KeycloakIssuer: getEnv("AUTH_KEYCLOAK_ISSUER", ""),
|
||||
KeycloakID: getEnv("AUTH_KEYCLOAK_ID", ""),
|
||||
KeycloakSecret: getEnv("AUTH_KEYCLOAK_SECRET", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
27
backend/internal/db/db.go
Normal file
27
backend/internal/db/db.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"evanpage-backend/internal/config"
|
||||
"evanpage-backend/internal/domain"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func Init(cfg *config.Config) *gorm.DB {
|
||||
var err error
|
||||
DB, err = gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to connect database: %v", err)
|
||||
}
|
||||
|
||||
if err := DB.AutoMigrate(&domain.User{}); err != nil {
|
||||
log.Fatalf("failed to migrate database: %v", err)
|
||||
}
|
||||
|
||||
log.Println("database connected and migrated")
|
||||
return DB
|
||||
}
|
||||
15
backend/internal/domain/user.go
Normal file
15
backend/internal/domain/user.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;not null" json:"username"`
|
||||
Email string `gorm:"uniqueIndex;not null" json:"email"`
|
||||
PasswordHash string `gorm:"not null" json:"-"`
|
||||
Role string `gorm:"default:'user';not null" json:"role"`
|
||||
KeycloakID *string `gorm:"uniqueIndex" json:"keycloakId,omitempty"`
|
||||
KeycloakEmail string `json:"keycloakEmail,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
68
backend/internal/handler/admin.go
Normal file
68
backend/internal/handler/admin.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"evanpage-backend/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AdminHandler struct {
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewAdminHandler(userService *service.UserService) *AdminHandler {
|
||||
return &AdminHandler{userService: userService}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ListUsers(c *gin.Context) {
|
||||
users, err := h.userService.ListUsers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"users": users})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) CreateUser(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Role string `json:"role" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.Register(req.Username, req.Email, req.Password, req.Role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) DeleteUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.DeleteUser(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "user deleted"})
|
||||
}
|
||||
156
backend/internal/handler/auth.go
Normal file
156
backend/internal/handler/auth.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"evanpage-backend/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
userService *service.UserService
|
||||
}
|
||||
|
||||
func NewAuthHandler(userService *service.UserService) *AuthHandler {
|
||||
return &AuthHandler{userService: userService}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.Register(req.Username, req.Email, req.Password, "user")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) LocalLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.ValidateLocalLogin(req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) LookupBinding(c *gin.Context) {
|
||||
var req struct {
|
||||
KeycloakID string `json:"keycloakId" binding:"required"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.LookupKeycloakBinding(req.KeycloakID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"bound": false})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"bound": true,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) BindKeycloak(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
KeycloakID string `json:"keycloakId" binding:"required"`
|
||||
KeycloakEmail string `json:"keycloakEmail"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.BindKeycloak(req.Username, req.Password, req.KeycloakID, req.KeycloakEmail)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"bound": true,
|
||||
"user": gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) InitAdmin(c *gin.Context) {
|
||||
count, err := h.userService.CountUsers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"})
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "already initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.Register(req.Username, req.Email, req.Password, "admin")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
})
|
||||
}
|
||||
32
backend/internal/handler/health.go
Normal file
32
backend/internal/handler/health.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type HealthHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewHealthHandler(db *gorm.DB) *HealthHandler {
|
||||
return &HealthHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Check(c *gin.Context) {
|
||||
sqlDB, err := h.db.DB()
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "DB connection error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
c.String(http.StatusInternalServerError, "DB unreachable: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "Database connected. Server time: %s", time.Now().Format(time.RFC3339))
|
||||
}
|
||||
23
backend/internal/middleware/auth.go
Normal file
23
backend/internal/middleware/auth.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AuthProxy() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID := c.GetHeader("X-User-Id")
|
||||
userRole := c.GetHeader("X-User-Role")
|
||||
|
||||
if userID == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("userID", userID)
|
||||
c.Set("userRole", userRole)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
33
backend/internal/middleware/cors.go
Normal file
33
backend/internal/middleware/cors.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CORS() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-User-Id, X-User-Role")
|
||||
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func Logger() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return param.TimeStamp.Format(time.RFC3339) + " " +
|
||||
param.Method + " " + param.Path + " " +
|
||||
param.ClientIP + " " + strconv.Itoa(param.StatusCode) + " " +
|
||||
param.Latency.String() + "\n"
|
||||
})
|
||||
}
|
||||
18
backend/internal/middleware/rbac.go
Normal file
18
backend/internal/middleware/rbac.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RequireRole(role string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userRole := c.GetString("userRole")
|
||||
if userRole != role {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "forbidden"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
62
backend/internal/repository/user.go
Normal file
62
backend/internal/repository/user.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"evanpage-backend/internal/domain"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UserRepository) Create(user *domain.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByID(id uint) (*domain.User, error) {
|
||||
var user domain.User
|
||||
err := r.db.First(&user, id).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByUsername(username string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
err := r.db.Where("username = ?", username).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByEmail(email string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByKeycloakID(keycloakID string) (*domain.User, error) {
|
||||
var user domain.User
|
||||
err := r.db.Where("keycloak_id = ?", keycloakID).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) ListAll() ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
err := r.db.Select("id", "username", "email", "role", "keycloak_id", "created_at", "updated_at").Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) Update(user *domain.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&domain.User{}, id).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) Count() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&domain.User{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
51
backend/internal/router/router.go
Normal file
51
backend/internal/router/router.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"evanpage-backend/internal/config"
|
||||
"evanpage-backend/internal/db"
|
||||
"evanpage-backend/internal/handler"
|
||||
"evanpage-backend/internal/middleware"
|
||||
"evanpage-backend/internal/repository"
|
||||
"evanpage-backend/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Setup(cfg *config.Config) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(middleware.Logger())
|
||||
r.Use(middleware.CORS())
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
userRepo := repository.NewUserRepository(db.DB)
|
||||
userService := service.NewUserService(userRepo)
|
||||
|
||||
authHandler := handler.NewAuthHandler(userService)
|
||||
healthHandler := handler.NewHealthHandler(db.DB)
|
||||
adminHandler := handler.NewAdminHandler(userService)
|
||||
|
||||
// Public routes
|
||||
r.POST("/api/auth/register", authHandler.Register)
|
||||
r.POST("/api/auth/local-login", authHandler.LocalLogin)
|
||||
r.POST("/api/auth/lookup-binding", authHandler.LookupBinding)
|
||||
r.POST("/api/auth/bind-keycloak", authHandler.BindKeycloak)
|
||||
r.POST("/api/auth/init", authHandler.InitAdmin)
|
||||
r.GET("/api/health", healthHandler.Check)
|
||||
|
||||
// Protected routes
|
||||
api := r.Group("/api")
|
||||
api.Use(middleware.AuthProxy())
|
||||
{
|
||||
}
|
||||
|
||||
// Admin routes
|
||||
admin := api.Group("/admin")
|
||||
admin.Use(middleware.RequireRole("admin"))
|
||||
{
|
||||
admin.GET("/users", adminHandler.ListUsers)
|
||||
admin.POST("/users", adminHandler.CreateUser)
|
||||
admin.DELETE("/users/:id", adminHandler.DeleteUser)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
106
backend/internal/service/user.go
Normal file
106
backend/internal/service/user.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"evanpage-backend/internal/domain"
|
||||
"evanpage-backend/internal/repository"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
repo *repository.UserRepository
|
||||
}
|
||||
|
||||
func NewUserService(repo *repository.UserRepository) *UserService {
|
||||
return &UserService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *UserService) Register(username, email, password, role string) (*domain.User, error) {
|
||||
_, err := s.repo.FindByUsername(username)
|
||||
if err == nil {
|
||||
return nil, errors.New("username already exists")
|
||||
}
|
||||
_, err = s.repo.FindByEmail(email)
|
||||
if err == nil {
|
||||
return nil, errors.New("email already exists")
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if role == "" {
|
||||
role = "user"
|
||||
}
|
||||
|
||||
user := &domain.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
PasswordHash: string(hash),
|
||||
Role: role,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) ValidateLocalLogin(username, password string) (*domain.User, error) {
|
||||
user, err := s.repo.FindByUsername(username)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) LookupKeycloakBinding(keycloakID string) (*domain.User, error) {
|
||||
return s.repo.FindByKeycloakID(keycloakID)
|
||||
}
|
||||
|
||||
func (s *UserService) BindKeycloak(username, password, keycloakID, keycloakEmail string) (*domain.User, error) {
|
||||
user, err := s.ValidateLocalLogin(username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.KeycloakID != nil && *user.KeycloakID != "" && *user.KeycloakID != keycloakID {
|
||||
return nil, errors.New("user already bound to another keycloak account")
|
||||
}
|
||||
|
||||
existing, err := s.repo.FindByKeycloakID(keycloakID)
|
||||
if err == nil && existing.ID != user.ID {
|
||||
return nil, errors.New("keycloak account already bound to another user")
|
||||
}
|
||||
|
||||
user.KeycloakID = &keycloakID
|
||||
user.KeycloakEmail = keycloakEmail
|
||||
if err := s.repo.Update(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) ListUsers() ([]domain.User, error) {
|
||||
return s.repo.ListAll()
|
||||
}
|
||||
|
||||
func (s *UserService) DeleteUser(id uint) error {
|
||||
return s.repo.Delete(id)
|
||||
}
|
||||
|
||||
func (s *UserService) CountUsers() (int64, error) {
|
||||
return s.repo.Count()
|
||||
}
|
||||
Reference in New Issue
Block a user