implement graphQL and init postgres
This commit is contained in:
@@ -2,6 +2,9 @@ package api
|
||||
|
||||
import (
|
||||
"omega-server/local/controller"
|
||||
"omega-server/local/graphql/handler"
|
||||
graphqlService "omega-server/local/graphql/service"
|
||||
"omega-server/local/service"
|
||||
"omega-server/local/utl/common"
|
||||
"omega-server/local/utl/configs"
|
||||
"omega-server/local/utl/logging"
|
||||
@@ -33,5 +36,35 @@ func Init(di *dig.Container, app *fiber.App) {
|
||||
logging.Panic("unable to bind routes")
|
||||
}
|
||||
|
||||
// Initialize GraphQL
|
||||
initGraphQL(di, groups)
|
||||
|
||||
controller.InitializeControllers(di)
|
||||
}
|
||||
|
||||
// initGraphQL initializes GraphQL endpoints
|
||||
func initGraphQL(di *dig.Container, groups fiber.Router) {
|
||||
err := di.Invoke(func(membershipService *service.MembershipService) error {
|
||||
// Create GraphQL service
|
||||
gqlService := graphqlService.NewGraphQLService(membershipService)
|
||||
_ = gqlService // Use the service (placeholder)
|
||||
|
||||
// Create GraphQL handler
|
||||
graphqlHandler := handler.NewGraphQLHandler(membershipService)
|
||||
|
||||
// Register GraphQL routes
|
||||
groups.Post("/graphql", graphqlHandler.Handle)
|
||||
|
||||
// GraphQL playground/schema endpoint
|
||||
groups.Get("/graphql", func(c *fiber.Ctx) error {
|
||||
return c.SendString(graphqlHandler.GetSchema())
|
||||
})
|
||||
|
||||
logging.Info("GraphQL endpoint initialized at /graphql")
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logging.Panic("failed to initialize GraphQL: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"omega-server/local/middleware"
|
||||
"omega-server/local/model"
|
||||
"omega-server/local/service"
|
||||
"omega-server/local/utl/common"
|
||||
"omega-server/local/utl/error_handler"
|
||||
@@ -52,7 +53,7 @@ func NewMembershipController(service *service.MembershipService, auth *middlewar
|
||||
// Login handles user login.
|
||||
func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
||||
type request struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
logging.Debug("Login request received")
|
||||
token, err := c.service.Login(ctx.UserContext(), req.Username, req.Password)
|
||||
token, err := c.service.Login(ctx.UserContext(), req.Email, req.Password)
|
||||
if err != nil {
|
||||
return c.errorHandler.HandleAuthError(ctx, err)
|
||||
}
|
||||
@@ -72,23 +73,35 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (mc *MembershipController) CreateUser(c *fiber.Ctx) error {
|
||||
type request struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
var req request
|
||||
var req model.UserCreateRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return mc.errorHandler.HandleParsingError(c, err)
|
||||
}
|
||||
|
||||
user, err := mc.service.CreateUser(c.UserContext(), req.Username, req.Password, req.Role)
|
||||
// Validate request
|
||||
if err := req.Validate(); err != nil {
|
||||
return mc.errorHandler.HandleValidationError(c, err, "user_create_request")
|
||||
}
|
||||
|
||||
// Map to domain model
|
||||
user, err := req.ToUser()
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleValidationError(c, err, "user_mapping")
|
||||
}
|
||||
|
||||
// Extract role names from request
|
||||
roleIDs := req.RoleIDs
|
||||
if len(roleIDs) == 0 {
|
||||
roleIDs = []string{"user"} // default role
|
||||
}
|
||||
|
||||
// Call service with domain model
|
||||
createdUser, err := mc.service.CreateUser(c.UserContext(), user, roleIDs)
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
return c.JSON(createdUser.ToResponse())
|
||||
}
|
||||
|
||||
// ListUsers lists all users.
|
||||
@@ -98,7 +111,13 @@ func (mc *MembershipController) ListUsers(c *fiber.Ctx) error {
|
||||
return mc.errorHandler.HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(users)
|
||||
// Convert to response format
|
||||
userResponses := make([]*model.UserResponse, len(users))
|
||||
for i, user := range users {
|
||||
userResponses[i] = user.ToResponse()
|
||||
}
|
||||
|
||||
return c.JSON(userResponses)
|
||||
}
|
||||
|
||||
// GetUser gets a single user by ID.
|
||||
@@ -113,7 +132,7 @@ func (mc *MembershipController) GetUser(c *fiber.Ctx) error {
|
||||
return mc.errorHandler.HandleNotFoundError(c, "User")
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
return c.JSON(user.ToResponse())
|
||||
}
|
||||
|
||||
// GetMe returns the currently authenticated user's details.
|
||||
@@ -128,10 +147,7 @@ func (mc *MembershipController) GetMe(c *fiber.Ctx) error {
|
||||
return mc.errorHandler.HandleNotFoundError(c, "User")
|
||||
}
|
||||
|
||||
// Sanitize the user object to not expose password
|
||||
user.PasswordHash = ""
|
||||
|
||||
return c.JSON(user)
|
||||
return c.JSON(user.ToResponse())
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user.
|
||||
@@ -156,17 +172,22 @@ func (mc *MembershipController) UpdateUser(c *fiber.Ctx) error {
|
||||
return mc.errorHandler.HandleUUIDError(c, "user ID")
|
||||
}
|
||||
|
||||
var req service.UpdateUserRequest
|
||||
var req model.UserUpdateRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return mc.errorHandler.HandleParsingError(c, err)
|
||||
}
|
||||
|
||||
user, err := mc.service.UpdateUser(c.UserContext(), id, req)
|
||||
// Validate request
|
||||
if err := req.Validate(); err != nil {
|
||||
return mc.errorHandler.HandleValidationError(c, err, "user_update_request")
|
||||
}
|
||||
|
||||
user, err := mc.service.UpdateUser(c.UserContext(), id, &req)
|
||||
if err != nil {
|
||||
return mc.errorHandler.HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
return c.JSON(user)
|
||||
return c.JSON(user.ToResponse())
|
||||
}
|
||||
|
||||
// GetRoles returns all available roles.
|
||||
|
||||
421
local/graphql/handler/handler.go
Normal file
421
local/graphql/handler/handler.go
Normal file
@@ -0,0 +1,421 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"omega-server/local/model"
|
||||
"omega-server/local/service"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// GraphQLHandler handles GraphQL requests
|
||||
type GraphQLHandler struct {
|
||||
membershipService *service.MembershipService
|
||||
}
|
||||
|
||||
// NewGraphQLHandler creates a new GraphQL handler
|
||||
func NewGraphQLHandler(membershipService *service.MembershipService) *GraphQLHandler {
|
||||
return &GraphQLHandler{
|
||||
membershipService: membershipService,
|
||||
}
|
||||
}
|
||||
|
||||
// GraphQLRequest represents a GraphQL request
|
||||
type GraphQLRequest struct {
|
||||
Query string `json:"query"`
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
}
|
||||
|
||||
// GraphQLResponse represents a GraphQL response
|
||||
type GraphQLResponse struct {
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Errors []GraphQLError `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// GraphQLError represents a GraphQL error
|
||||
type GraphQLError struct {
|
||||
Message string `json:"message"`
|
||||
Path []string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// Handle processes GraphQL requests
|
||||
func (h *GraphQLHandler) Handle(c *fiber.Ctx) error {
|
||||
var req GraphQLRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Invalid request body"}},
|
||||
})
|
||||
}
|
||||
|
||||
// Basic query parsing and handling
|
||||
ctx := c.UserContext()
|
||||
|
||||
// Simple query routing based on query string
|
||||
query := strings.TrimSpace(req.Query)
|
||||
|
||||
switch {
|
||||
case strings.Contains(query, "mutation") && strings.Contains(query, "login"):
|
||||
return h.handleLogin(c, ctx, req)
|
||||
case strings.Contains(query, "mutation") && strings.Contains(query, "createUser"):
|
||||
return h.handleCreateUser(c, ctx, req)
|
||||
case strings.Contains(query, "mutation") && strings.Contains(query, "createProject"):
|
||||
return h.handleCreateProject(c, ctx, req)
|
||||
case strings.Contains(query, "mutation") && strings.Contains(query, "createTask"):
|
||||
return h.handleCreateTask(c, ctx, req)
|
||||
case strings.Contains(query, "query") && strings.Contains(query, "me"):
|
||||
return h.handleMe(c, ctx, req)
|
||||
case strings.Contains(query, "query") && strings.Contains(query, "users"):
|
||||
return h.handleUsers(c, ctx, req)
|
||||
case strings.Contains(query, "query") && strings.Contains(query, "projects"):
|
||||
return h.handleProjects(c, ctx, req)
|
||||
case strings.Contains(query, "query") && strings.Contains(query, "tasks"):
|
||||
return h.handleTasks(c, ctx, req)
|
||||
default:
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Query not supported"}},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogin handles login mutations
|
||||
func (h *GraphQLHandler) handleLogin(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||
// Extract variables
|
||||
email, ok := req.Variables["email"].(string)
|
||||
if !ok {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Email is required"}},
|
||||
})
|
||||
}
|
||||
|
||||
password, ok := req.Variables["password"].(string)
|
||||
if !ok {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Password is required"}},
|
||||
})
|
||||
}
|
||||
|
||||
// Call service
|
||||
token, err := h.membershipService.Login(ctx, email, password)
|
||||
if err != nil {
|
||||
return c.Status(401).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Invalid credentials"}},
|
||||
})
|
||||
}
|
||||
|
||||
// Mock user data for now
|
||||
userData := map[string]interface{}{
|
||||
"id": "1",
|
||||
"email": email,
|
||||
"fullName": "User",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"login": map[string]interface{}{
|
||||
"token": token,
|
||||
"user": userData,
|
||||
},
|
||||
}
|
||||
|
||||
return c.JSON(GraphQLResponse{Data: response})
|
||||
}
|
||||
|
||||
// handleCreateUser handles user creation mutations
|
||||
func (h *GraphQLHandler) handleCreateUser(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||
// Extract variables
|
||||
email, ok := req.Variables["email"].(string)
|
||||
if !ok {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Email is required"}},
|
||||
})
|
||||
}
|
||||
|
||||
password, ok := req.Variables["password"].(string)
|
||||
if !ok {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Password is required"}},
|
||||
})
|
||||
}
|
||||
|
||||
fullName, ok := req.Variables["fullName"].(string)
|
||||
if !ok {
|
||||
fullName = ""
|
||||
}
|
||||
|
||||
// Create domain model
|
||||
user := &model.User{
|
||||
Email: email,
|
||||
FullName: fullName,
|
||||
}
|
||||
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: err.Error()}},
|
||||
})
|
||||
}
|
||||
|
||||
// Call service
|
||||
createdUser, err := h.membershipService.CreateUser(ctx, user, []string{"user"})
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: err.Error()}},
|
||||
})
|
||||
}
|
||||
|
||||
// Convert user to response format
|
||||
userData := map[string]interface{}{
|
||||
"id": createdUser.ID,
|
||||
"email": createdUser.Email,
|
||||
"fullName": createdUser.FullName,
|
||||
"createdAt": createdUser.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
"updatedAt": createdUser.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"createUser": userData,
|
||||
}
|
||||
|
||||
return c.JSON(GraphQLResponse{Data: response})
|
||||
}
|
||||
|
||||
// handleMe handles me queries
|
||||
func (h *GraphQLHandler) handleMe(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||
// This would typically extract user ID from JWT token
|
||||
// For now, return mock data
|
||||
userData := map[string]interface{}{
|
||||
"id": "1",
|
||||
"email": "admin@example.com",
|
||||
"fullName": "System Administrator",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"me": userData,
|
||||
}
|
||||
|
||||
return c.JSON(GraphQLResponse{Data: response})
|
||||
}
|
||||
|
||||
// handleUsers handles users queries
|
||||
func (h *GraphQLHandler) handleUsers(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||
// Call service
|
||||
users, err := h.membershipService.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Failed to fetch users"}},
|
||||
})
|
||||
}
|
||||
|
||||
// Convert users to response format
|
||||
usersData := make([]map[string]interface{}, len(users))
|
||||
for i, user := range users {
|
||||
usersData[i] = map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"email": user.Email,
|
||||
"fullName": user.FullName,
|
||||
"createdAt": user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
"updatedAt": user.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"users": usersData,
|
||||
}
|
||||
|
||||
return c.JSON(GraphQLResponse{Data: response})
|
||||
}
|
||||
|
||||
// handleCreateProject handles project creation mutations
|
||||
func (h *GraphQLHandler) handleCreateProject(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||
// Extract variables
|
||||
name, ok := req.Variables["name"].(string)
|
||||
if !ok {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Name is required"}},
|
||||
})
|
||||
}
|
||||
|
||||
ownerId, ok := req.Variables["ownerId"].(string)
|
||||
if !ok {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Owner ID is required"}},
|
||||
})
|
||||
}
|
||||
|
||||
description, _ := req.Variables["description"].(string)
|
||||
|
||||
// Mock project data for now
|
||||
projectData := map[string]interface{}{
|
||||
"id": "mock-project-id",
|
||||
"name": name,
|
||||
"description": description,
|
||||
"ownerId": ownerId,
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"createProject": projectData,
|
||||
}
|
||||
|
||||
return c.JSON(GraphQLResponse{Data: response})
|
||||
}
|
||||
|
||||
// handleCreateTask handles task creation mutations
|
||||
func (h *GraphQLHandler) handleCreateTask(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||
// Extract variables
|
||||
title, ok := req.Variables["title"].(string)
|
||||
if !ok {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Title is required"}},
|
||||
})
|
||||
}
|
||||
|
||||
projectId, ok := req.Variables["projectId"].(string)
|
||||
if !ok {
|
||||
return c.Status(400).JSON(GraphQLResponse{
|
||||
Errors: []GraphQLError{{Message: "Project ID is required"}},
|
||||
})
|
||||
}
|
||||
|
||||
description, _ := req.Variables["description"].(string)
|
||||
status, _ := req.Variables["status"].(string)
|
||||
if status == "" {
|
||||
status = "todo"
|
||||
}
|
||||
priority, _ := req.Variables["priority"].(string)
|
||||
if priority == "" {
|
||||
priority = "medium"
|
||||
}
|
||||
|
||||
// Mock task data for now
|
||||
taskData := map[string]interface{}{
|
||||
"id": "mock-task-id",
|
||||
"title": title,
|
||||
"description": description,
|
||||
"status": status,
|
||||
"priority": priority,
|
||||
"projectId": projectId,
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"createTask": taskData,
|
||||
}
|
||||
|
||||
return c.JSON(GraphQLResponse{Data: response})
|
||||
}
|
||||
|
||||
// handleProjects handles projects queries
|
||||
func (h *GraphQLHandler) handleProjects(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||
// Mock empty projects list for now
|
||||
response := map[string]interface{}{
|
||||
"projects": []interface{}{},
|
||||
}
|
||||
|
||||
return c.JSON(GraphQLResponse{Data: response})
|
||||
}
|
||||
|
||||
// handleTasks handles tasks queries
|
||||
func (h *GraphQLHandler) handleTasks(c *fiber.Ctx, ctx context.Context, req GraphQLRequest) error {
|
||||
// Mock empty tasks list for now
|
||||
response := map[string]interface{}{
|
||||
"tasks": []interface{}{},
|
||||
}
|
||||
|
||||
return c.JSON(GraphQLResponse{Data: response})
|
||||
}
|
||||
|
||||
// GetSchema returns the GraphQL schema
|
||||
func (h *GraphQLHandler) GetSchema() string {
|
||||
return `
|
||||
# Core Types
|
||||
type User {
|
||||
id: String!
|
||||
email: String!
|
||||
fullName: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Project {
|
||||
id: String!
|
||||
name: String!
|
||||
description: String
|
||||
ownerId: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Task {
|
||||
id: String!
|
||||
title: String!
|
||||
description: String
|
||||
status: String!
|
||||
priority: String!
|
||||
projectId: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
# Input Types
|
||||
input LoginInput {
|
||||
email: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input UserCreateInput {
|
||||
email: String!
|
||||
fullName: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input ProjectCreateInput {
|
||||
name: String!
|
||||
description: String
|
||||
ownerId: String!
|
||||
}
|
||||
|
||||
input TaskCreateInput {
|
||||
title: String!
|
||||
description: String
|
||||
status: String
|
||||
priority: String
|
||||
projectId: String!
|
||||
}
|
||||
|
||||
# Response Types
|
||||
type AuthResponse {
|
||||
token: String!
|
||||
user: User!
|
||||
}
|
||||
|
||||
type MessageResponse {
|
||||
message: String!
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
# Queries
|
||||
type Query {
|
||||
me: User!
|
||||
users: [User!]!
|
||||
user(id: String!): User
|
||||
projects: [Project!]!
|
||||
project(id: String!): Project
|
||||
tasks(projectId: String): [Task!]!
|
||||
task(id: String!): Task
|
||||
}
|
||||
|
||||
# Mutations
|
||||
type Mutation {
|
||||
login(input: LoginInput!): AuthResponse!
|
||||
createUser(input: UserCreateInput!): User!
|
||||
createProject(input: ProjectCreateInput!): Project!
|
||||
createTask(input: TaskCreateInput!): Task!
|
||||
}
|
||||
`
|
||||
}
|
||||
100
local/graphql/schema/schema.graphql
Normal file
100
local/graphql/schema/schema.graphql
Normal file
@@ -0,0 +1,100 @@
|
||||
# Minimal GraphQL Schema for Phase 1
|
||||
|
||||
# Core Types
|
||||
type User {
|
||||
id: String!
|
||||
email: String!
|
||||
fullName: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Project {
|
||||
id: String!
|
||||
name: String!
|
||||
description: String
|
||||
ownerId: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Task {
|
||||
id: String!
|
||||
title: String!
|
||||
description: String
|
||||
status: String!
|
||||
priority: String!
|
||||
projectId: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
# Input Types
|
||||
input LoginInput {
|
||||
email: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input UserCreateInput {
|
||||
email: String!
|
||||
fullName: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input ProjectCreateInput {
|
||||
name: String!
|
||||
description: String
|
||||
ownerId: String!
|
||||
}
|
||||
|
||||
input TaskCreateInput {
|
||||
title: String!
|
||||
description: String
|
||||
status: String
|
||||
priority: String
|
||||
projectId: String!
|
||||
}
|
||||
|
||||
# Response Types
|
||||
type AuthResponse {
|
||||
token: String!
|
||||
user: User!
|
||||
}
|
||||
|
||||
type MessageResponse {
|
||||
message: String!
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
# Queries
|
||||
type Query {
|
||||
# Authentication
|
||||
me: User!
|
||||
|
||||
# Users
|
||||
users: [User!]!
|
||||
user(id: String!): User
|
||||
|
||||
# Projects
|
||||
projects: [Project!]!
|
||||
project(id: String!): Project
|
||||
|
||||
# Tasks
|
||||
tasks(projectId: String): [Task!]!
|
||||
task(id: String!): Task
|
||||
}
|
||||
|
||||
# Mutations
|
||||
type Mutation {
|
||||
# Authentication
|
||||
login(input: LoginInput!): AuthResponse!
|
||||
|
||||
# Users
|
||||
createUser(input: UserCreateInput!): User!
|
||||
|
||||
# Projects
|
||||
createProject(input: ProjectCreateInput!): Project!
|
||||
|
||||
# Tasks
|
||||
createTask(input: TaskCreateInput!): Task!
|
||||
}
|
||||
173
local/graphql/service/service.go
Normal file
173
local/graphql/service/service.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"omega-server/local/model"
|
||||
"omega-server/local/service"
|
||||
)
|
||||
|
||||
// GraphQLService provides GraphQL-specific business logic
|
||||
type GraphQLService struct {
|
||||
membershipService *service.MembershipService
|
||||
}
|
||||
|
||||
// NewGraphQLService creates a new GraphQL service
|
||||
func NewGraphQLService(membershipService *service.MembershipService) *GraphQLService {
|
||||
return &GraphQLService{
|
||||
membershipService: membershipService,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthResponse represents authentication response
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
User *model.User `json:"user"`
|
||||
}
|
||||
|
||||
// MessageResponse represents a generic message response
|
||||
type MessageResponse struct {
|
||||
Message string `json:"message"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// Login handles user authentication
|
||||
func (s *GraphQLService) Login(ctx context.Context, email, password string) (*AuthResponse, error) {
|
||||
token, err := s.membershipService.Login(ctx, email, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For now, return a mock user. In a full implementation, we'd get the user from the token
|
||||
user := &model.User{
|
||||
BaseModel: model.BaseModel{
|
||||
ID: "mock-user-id",
|
||||
},
|
||||
Email: email,
|
||||
FullName: "Mock User",
|
||||
}
|
||||
|
||||
return &AuthResponse{
|
||||
Token: token,
|
||||
User: user,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateUser handles user creation
|
||||
func (s *GraphQLService) CreateUser(ctx context.Context, email, password, fullName string) (*model.User, error) {
|
||||
// Create domain model
|
||||
user := &model.User{
|
||||
Email: email,
|
||||
FullName: fullName,
|
||||
}
|
||||
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.membershipService.CreateUser(ctx, user, []string{"user"})
|
||||
}
|
||||
|
||||
// GetUsers retrieves all users
|
||||
func (s *GraphQLService) GetUsers(ctx context.Context) ([]*model.User, error) {
|
||||
return s.membershipService.ListUsers(ctx)
|
||||
}
|
||||
|
||||
// GetUser retrieves a specific user by ID
|
||||
func (s *GraphQLService) GetUser(ctx context.Context, id string) (*model.User, error) {
|
||||
// This would need to be implemented in the membership service
|
||||
// For now, return a mock user
|
||||
return &model.User{
|
||||
BaseModel: model.BaseModel{
|
||||
ID: id,
|
||||
},
|
||||
Email: "mock@example.com",
|
||||
FullName: "Mock User",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetMe retrieves the current authenticated user
|
||||
func (s *GraphQLService) GetMe(ctx context.Context, userID string) (*model.User, error) {
|
||||
// This would typically extract user ID from JWT token
|
||||
// For now, return a mock user
|
||||
return &model.User{
|
||||
BaseModel: model.BaseModel{
|
||||
ID: userID,
|
||||
},
|
||||
Email: "current@example.com",
|
||||
FullName: "Current User",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateProject handles project creation
|
||||
func (s *GraphQLService) CreateProject(ctx context.Context, name, description, ownerID string) (*model.Project, error) {
|
||||
// This would need to be implemented when we have project service
|
||||
// For now, return a mock project
|
||||
return &model.Project{
|
||||
BaseModel: model.BaseModel{
|
||||
ID: "mock-project-id",
|
||||
},
|
||||
Name: name,
|
||||
Description: description,
|
||||
OwnerID: ownerID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProjects retrieves all projects
|
||||
func (s *GraphQLService) GetProjects(ctx context.Context) ([]*model.Project, error) {
|
||||
// This would need to be implemented when we have project service
|
||||
// For now, return empty slice
|
||||
return []*model.Project{}, nil
|
||||
}
|
||||
|
||||
// GetProject retrieves a specific project by ID
|
||||
func (s *GraphQLService) GetProject(ctx context.Context, id string) (*model.Project, error) {
|
||||
// This would need to be implemented when we have project service
|
||||
// For now, return a mock project
|
||||
return &model.Project{
|
||||
BaseModel: model.BaseModel{
|
||||
ID: id,
|
||||
},
|
||||
Name: "Mock Project",
|
||||
Description: "Mock project description",
|
||||
OwnerID: "mock-owner-id",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateTask handles task creation
|
||||
func (s *GraphQLService) CreateTask(ctx context.Context, title, description, status, priority, projectID string) (*model.Task, error) {
|
||||
// This would need to be implemented when we have task service
|
||||
// For now, return a mock task
|
||||
return &model.Task{
|
||||
BaseModel: model.BaseModel{
|
||||
ID: "mock-task-id",
|
||||
},
|
||||
Title: title,
|
||||
Description: description,
|
||||
Status: model.TaskStatus(status),
|
||||
Priority: model.TaskPriority(priority),
|
||||
ProjectID: projectID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTasks retrieves tasks, optionally filtered by project ID
|
||||
func (s *GraphQLService) GetTasks(ctx context.Context, projectID *string) ([]*model.Task, error) {
|
||||
// This would need to be implemented when we have task service
|
||||
// For now, return empty slice
|
||||
return []*model.Task{}, nil
|
||||
}
|
||||
|
||||
// GetTask retrieves a specific task by ID
|
||||
func (s *GraphQLService) GetTask(ctx context.Context, id string) (*model.Task, error) {
|
||||
// This would need to be implemented when we have task service
|
||||
// For now, return a mock task
|
||||
return &model.Task{
|
||||
BaseModel: model.BaseModel{
|
||||
ID: id,
|
||||
},
|
||||
Title: "Mock Task",
|
||||
Description: "Mock task description",
|
||||
Status: model.TaskStatusTodo,
|
||||
Priority: model.TaskPriorityMedium,
|
||||
ProjectID: "mock-project-id",
|
||||
}, nil
|
||||
}
|
||||
@@ -26,13 +26,13 @@ type CachedUserInfo struct {
|
||||
|
||||
// AuthMiddleware provides authentication and permission middleware.
|
||||
type AuthMiddleware struct {
|
||||
membershipService service.MembershipServiceInterface
|
||||
membershipService *service.MembershipService
|
||||
cache *cache.InMemoryCache
|
||||
securityMW *security.SecurityMiddleware
|
||||
}
|
||||
|
||||
// NewAuthMiddleware creates a new AuthMiddleware.
|
||||
func NewAuthMiddleware(ms service.MembershipServiceInterface, cache *cache.InMemoryCache) *AuthMiddleware {
|
||||
func NewAuthMiddleware(ms *service.MembershipService, cache *cache.InMemoryCache) *AuthMiddleware {
|
||||
auth := &AuthMiddleware{
|
||||
membershipService: ms,
|
||||
cache: cache,
|
||||
@@ -201,7 +201,7 @@ func (m *AuthMiddleware) getCachedUserInfo(ctx context.Context, userID string) (
|
||||
|
||||
userInfo := &CachedUserInfo{
|
||||
UserID: userID,
|
||||
Username: user.Username,
|
||||
Username: user.FullName,
|
||||
Roles: roleNames,
|
||||
RoleNames: roleNames,
|
||||
Permissions: permissions,
|
||||
|
||||
@@ -44,22 +44,22 @@ type AuditLogCreateRequest struct {
|
||||
|
||||
// AuditLogInfo represents public audit log information
|
||||
type AuditLogInfo struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
UserEmail string `json:"userEmail,omitempty"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
IPAddress string `json:"ipAddress"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Success bool `json:"success"`
|
||||
ErrorMsg string `json:"errorMsg,omitempty"`
|
||||
Duration int64 `json:"duration,omitempty"`
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"userId"`
|
||||
UserEmail string `json:"userEmail,omitempty"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resourceId"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
IPAddress string `json:"ipAddress"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Success bool `json:"success"`
|
||||
ErrorMsg string `json:"errorMsg,omitempty"`
|
||||
Duration int64 `json:"duration,omitempty"`
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating an audit log
|
||||
@@ -122,26 +122,26 @@ func (al *AuditLog) GetDetailsAsJSON() (string, error) {
|
||||
// ToAuditLogInfo converts AuditLog to AuditLogInfo (public information)
|
||||
func (al *AuditLog) ToAuditLogInfo() AuditLogInfo {
|
||||
info := AuditLogInfo{
|
||||
ID: al.ID,
|
||||
UserID: al.UserID,
|
||||
Action: al.Action,
|
||||
Resource: al.Resource,
|
||||
ResourceID: al.ResourceID,
|
||||
Details: al.GetDetails(),
|
||||
IPAddress: al.IPAddress,
|
||||
UserAgent: al.UserAgent,
|
||||
Success: al.Success,
|
||||
ErrorMsg: al.ErrorMsg,
|
||||
Duration: al.Duration,
|
||||
SessionID: al.SessionID,
|
||||
RequestID: al.RequestID,
|
||||
DateCreated: al.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
ID: al.ID,
|
||||
UserID: al.UserID,
|
||||
Action: al.Action,
|
||||
Resource: al.Resource,
|
||||
ResourceID: al.ResourceID,
|
||||
Details: al.GetDetails(),
|
||||
IPAddress: al.IPAddress,
|
||||
UserAgent: al.UserAgent,
|
||||
Success: al.Success,
|
||||
ErrorMsg: al.ErrorMsg,
|
||||
Duration: al.Duration,
|
||||
SessionID: al.SessionID,
|
||||
RequestID: al.RequestID,
|
||||
CreatedAt: al.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
// Include user information if available
|
||||
if al.User != nil {
|
||||
info.UserEmail = al.User.Email
|
||||
info.UserName = al.User.Name
|
||||
info.UserName = al.User.FullName
|
||||
}
|
||||
|
||||
return info
|
||||
|
||||
@@ -8,22 +8,22 @@ import (
|
||||
|
||||
// BaseModel provides common fields for all database models
|
||||
type BaseModel struct {
|
||||
ID string `json:"id" gorm:"primary_key;type:varchar(36)"`
|
||||
DateCreated time.Time `json:"dateCreated" gorm:"not null"`
|
||||
DateUpdated time.Time `json:"dateUpdated" gorm:"not null"`
|
||||
ID string `json:"id" gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"not null;default:now()"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"not null;default:now()"`
|
||||
}
|
||||
|
||||
// Init initializes base model with DateCreated, DateUpdated, and ID values
|
||||
// Init initializes base model with CreatedAt, UpdatedAt, and ID values
|
||||
func (bm *BaseModel) Init() {
|
||||
now := time.Now().UTC()
|
||||
bm.ID = uuid.NewString()
|
||||
bm.DateCreated = now
|
||||
bm.DateUpdated = now
|
||||
bm.CreatedAt = now
|
||||
bm.UpdatedAt = now
|
||||
}
|
||||
|
||||
// UpdateTimestamp updates the DateUpdated field
|
||||
// UpdateTimestamp updates the UpdatedAt field
|
||||
func (bm *BaseModel) UpdateTimestamp() {
|
||||
bm.DateUpdated = time.Now().UTC()
|
||||
bm.UpdatedAt = time.Now().UTC()
|
||||
}
|
||||
|
||||
// BeforeCreate is a GORM hook that runs before creating a record
|
||||
@@ -76,7 +76,7 @@ func DefaultParams() Params {
|
||||
return Params{
|
||||
Page: 1,
|
||||
Limit: 10,
|
||||
SortBy: "dateCreated",
|
||||
SortBy: "created_at",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func (p *Params) Validate() {
|
||||
p.Limit = 10
|
||||
}
|
||||
if p.SortBy == "" {
|
||||
p.SortBy = "dateCreated"
|
||||
p.SortBy = "created_at"
|
||||
}
|
||||
if p.SortOrder != "asc" && p.SortOrder != "desc" {
|
||||
p.SortOrder = "desc"
|
||||
|
||||
144
local/model/integration.go
Normal file
144
local/model/integration.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Integration represents a third-party integration configuration
|
||||
type Integration struct {
|
||||
BaseModel
|
||||
ProjectID string `json:"project_id" gorm:"not null;type:uuid;index;references:projects(id);onDelete:CASCADE"`
|
||||
Type string `json:"type" gorm:"not null;type:varchar(50)"`
|
||||
Config json.RawMessage `json:"config" gorm:"type:jsonb;not null"`
|
||||
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
|
||||
}
|
||||
|
||||
// IntegrationCreateRequest represents the request to create a new integration
|
||||
type IntegrationCreateRequest struct {
|
||||
ProjectID string `json:"project_id" validate:"required,uuid"`
|
||||
Type string `json:"type" validate:"required,min=1,max=50"`
|
||||
Config map[string]interface{} `json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// IntegrationUpdateRequest represents the request to update an integration
|
||||
type IntegrationUpdateRequest struct {
|
||||
Config map[string]interface{} `json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// IntegrationInfo represents public integration information
|
||||
type IntegrationInfo struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
Type string `json:"type"`
|
||||
Config map[string]interface{} `json:"config"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating an integration
|
||||
func (i *Integration) BeforeCreate(tx *gorm.DB) error {
|
||||
i.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize type
|
||||
i.Type = strings.ToLower(strings.TrimSpace(i.Type))
|
||||
|
||||
return i.Validate()
|
||||
}
|
||||
|
||||
// BeforeUpdate is called before updating an integration
|
||||
func (i *Integration) BeforeUpdate(tx *gorm.DB) error {
|
||||
i.BaseModel.BeforeUpdate()
|
||||
|
||||
// Normalize type if it's being updated
|
||||
if i.Type != "" {
|
||||
i.Type = strings.ToLower(strings.TrimSpace(i.Type))
|
||||
}
|
||||
|
||||
return i.Validate()
|
||||
}
|
||||
|
||||
// Validate validates integration data
|
||||
func (i *Integration) Validate() error {
|
||||
if i.ProjectID == "" {
|
||||
return errors.New("project_id is required")
|
||||
}
|
||||
|
||||
if i.Type == "" {
|
||||
return errors.New("type is required")
|
||||
}
|
||||
|
||||
if len(i.Type) > 50 {
|
||||
return errors.New("type must not exceed 50 characters")
|
||||
}
|
||||
|
||||
if len(i.Config) == 0 {
|
||||
return errors.New("config is required")
|
||||
}
|
||||
|
||||
// Validate that config is valid JSON
|
||||
var configMap map[string]interface{}
|
||||
if err := json.Unmarshal(i.Config, &configMap); err != nil {
|
||||
return errors.New("config must be valid JSON")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToIntegrationInfo converts Integration to IntegrationInfo (public information)
|
||||
func (i *Integration) ToIntegrationInfo() IntegrationInfo {
|
||||
integrationInfo := IntegrationInfo{
|
||||
ID: i.ID,
|
||||
ProjectID: i.ProjectID,
|
||||
Type: i.Type,
|
||||
CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: i.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
// Parse config JSON to map
|
||||
var configMap map[string]interface{}
|
||||
if err := json.Unmarshal(i.Config, &configMap); err == nil {
|
||||
integrationInfo.Config = configMap
|
||||
}
|
||||
|
||||
return integrationInfo
|
||||
}
|
||||
|
||||
// SetConfig sets the configuration from a map
|
||||
func (i *Integration) SetConfig(config map[string]interface{}) error {
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.Config = configJSON
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig gets the configuration as a map
|
||||
func (i *Integration) GetConfig() (map[string]interface{}, error) {
|
||||
var configMap map[string]interface{}
|
||||
err := json.Unmarshal(i.Config, &configMap)
|
||||
return configMap, err
|
||||
}
|
||||
|
||||
// GetConfigValue gets a specific configuration value
|
||||
func (i *Integration) GetConfigValue(key string) (interface{}, error) {
|
||||
configMap, err := i.GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return configMap[key], nil
|
||||
}
|
||||
|
||||
// SetConfigValue sets a specific configuration value
|
||||
func (i *Integration) SetConfigValue(key string, value interface{}) error {
|
||||
configMap, err := i.GetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configMap[key] = value
|
||||
return i.SetConfig(configMap)
|
||||
}
|
||||
@@ -156,10 +156,10 @@ func (f *MembershipFilter) GetSorting() (field string, desc bool) {
|
||||
|
||||
// Map common sort fields to database column names
|
||||
switch f.SortBy {
|
||||
case "dateCreated":
|
||||
field = "date_created"
|
||||
case "dateUpdated":
|
||||
field = "date_updated"
|
||||
case "created_at":
|
||||
field = "created_at"
|
||||
case "updated_at":
|
||||
field = "updated_at"
|
||||
case "username":
|
||||
field = "username"
|
||||
case "email":
|
||||
|
||||
@@ -42,7 +42,7 @@ type PermissionInfo struct {
|
||||
Active bool `json:"active"`
|
||||
System bool `json:"system"`
|
||||
RoleCount int64 `json:"roleCount"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a permission
|
||||
@@ -132,7 +132,7 @@ func (p *Permission) ToPermissionInfo() PermissionInfo {
|
||||
Category: p.Category,
|
||||
Active: p.Active,
|
||||
System: p.System,
|
||||
DateCreated: p.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
148
local/model/project.go
Normal file
148
local/model/project.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Project represents a project in the system
|
||||
type Project struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null;type:varchar(255)"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
OwnerID string `json:"owner_id" gorm:"not null;type:uuid;index;references:users(id)"`
|
||||
TypeID string `json:"type_id" gorm:"not null;type:uuid;index;references:types(id)"`
|
||||
Owner User `json:"owner,omitempty" gorm:"foreignKey:OwnerID"`
|
||||
Type Type `json:"type,omitempty" gorm:"foreignKey:TypeID"`
|
||||
Tasks []Task `json:"tasks,omitempty" gorm:"foreignKey:ProjectID"`
|
||||
Members []User `json:"members,omitempty" gorm:"many2many:project_members;"`
|
||||
}
|
||||
|
||||
// ProjectCreateRequest represents the request to create a new project
|
||||
type ProjectCreateRequest struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
OwnerID string `json:"owner_id" validate:"required,uuid"`
|
||||
TypeID string `json:"type_id" validate:"required,uuid"`
|
||||
}
|
||||
|
||||
// ProjectUpdateRequest represents the request to update a project
|
||||
type ProjectUpdateRequest struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
||||
TypeID *string `json:"type_id,omitempty" validate:"omitempty,uuid"`
|
||||
}
|
||||
|
||||
// ProjectInfo represents public project information
|
||||
type ProjectInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
TypeID string `json:"type_id"`
|
||||
Owner UserInfo `json:"owner,omitempty"`
|
||||
Type TypeInfo `json:"type,omitempty"`
|
||||
TaskCount int64 `json:"task_count"`
|
||||
MemberCount int64 `json:"member_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ProjectMember represents the many-to-many relationship between projects and users
|
||||
type ProjectMember struct {
|
||||
ProjectID string `json:"project_id" gorm:"type:uuid;primaryKey"`
|
||||
UserID string `json:"user_id" gorm:"type:uuid;primaryKey"`
|
||||
RoleID string `json:"role_id" gorm:"type:uuid;not null;references:roles(id)"`
|
||||
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
|
||||
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Role Role `json:"role,omitempty" gorm:"foreignKey:RoleID"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a project
|
||||
func (p *Project) BeforeCreate(tx *gorm.DB) error {
|
||||
p.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize name and description
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
p.Description = strings.TrimSpace(p.Description)
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
// BeforeUpdate is called before updating a project
|
||||
func (p *Project) BeforeUpdate(tx *gorm.DB) error {
|
||||
p.BaseModel.BeforeUpdate()
|
||||
|
||||
// Normalize fields if they're being updated
|
||||
if p.Name != "" {
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
}
|
||||
if p.Description != "" {
|
||||
p.Description = strings.TrimSpace(p.Description)
|
||||
}
|
||||
|
||||
return p.Validate()
|
||||
}
|
||||
|
||||
// Validate validates project data
|
||||
func (p *Project) Validate() error {
|
||||
if p.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
if len(p.Name) > 255 {
|
||||
return errors.New("name must not exceed 255 characters")
|
||||
}
|
||||
|
||||
if p.OwnerID == "" {
|
||||
return errors.New("owner_id is required")
|
||||
}
|
||||
|
||||
if p.TypeID == "" {
|
||||
return errors.New("type_id is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToProjectInfo converts Project to ProjectInfo (public information)
|
||||
func (p *Project) ToProjectInfo() ProjectInfo {
|
||||
projectInfo := ProjectInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Description: p.Description,
|
||||
OwnerID: p.OwnerID,
|
||||
TypeID: p.TypeID,
|
||||
CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: p.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
// Add owner info if loaded
|
||||
if p.Owner.ID != "" {
|
||||
projectInfo.Owner = p.Owner.ToUserInfo()
|
||||
}
|
||||
|
||||
// Add type info if loaded
|
||||
if p.Type.ID != "" {
|
||||
projectInfo.Type = p.Type.ToTypeInfo()
|
||||
}
|
||||
|
||||
return projectInfo
|
||||
}
|
||||
|
||||
// HasMember checks if a user is a member of the project
|
||||
func (p *Project) HasMember(userID string) bool {
|
||||
for _, member := range p.Members {
|
||||
if member.ID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsOwner checks if a user is the owner of the project
|
||||
func (p *Project) IsOwner(userID string) bool {
|
||||
return p.OwnerID == userID
|
||||
}
|
||||
@@ -42,7 +42,7 @@ type RoleInfo struct {
|
||||
System bool `json:"system"`
|
||||
Permissions []PermissionInfo `json:"permissions"`
|
||||
UserCount int64 `json:"userCount"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a role
|
||||
@@ -120,7 +120,7 @@ func (r *Role) ToRoleInfo() RoleInfo {
|
||||
Active: r.Active,
|
||||
System: r.System,
|
||||
Permissions: make([]PermissionInfo, len(r.Permissions)),
|
||||
DateCreated: r.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
// Convert permissions
|
||||
|
||||
@@ -86,7 +86,7 @@ type SecurityEventInfo struct {
|
||||
ResolverName string `json:"resolverName,omitempty"`
|
||||
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a security event
|
||||
@@ -202,19 +202,19 @@ func (se *SecurityEvent) ToSecurityEventInfo() SecurityEventInfo {
|
||||
ResolvedBy: se.ResolvedBy,
|
||||
ResolvedAt: se.ResolvedAt,
|
||||
Notes: se.Notes,
|
||||
DateCreated: se.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
CreatedAt: se.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
// Include user information if available
|
||||
if se.User != nil {
|
||||
info.UserEmail = se.User.Email
|
||||
info.UserName = se.User.Name
|
||||
info.UserName = se.User.FullName
|
||||
}
|
||||
|
||||
// Include resolver information if available
|
||||
if se.Resolver != nil {
|
||||
info.ResolverEmail = se.Resolver.Email
|
||||
info.ResolverName = se.Resolver.Name
|
||||
info.ResolverName = se.Resolver.FullName
|
||||
}
|
||||
|
||||
return info
|
||||
|
||||
@@ -56,7 +56,7 @@ type SystemConfigInfo struct {
|
||||
DataType string `json:"dataType"`
|
||||
IsEditable bool `json:"isEditable"`
|
||||
IsSecret bool `json:"isSecret"`
|
||||
DateCreated string `json:"dateCreated"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
DateModified string `json:"dateModified"`
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ func (sc *SystemConfig) ToSystemConfigInfo() SystemConfigInfo {
|
||||
DataType: sc.DataType,
|
||||
IsEditable: sc.IsEditable,
|
||||
IsSecret: sc.IsSecret,
|
||||
DateCreated: sc.DateCreated.Format("2006-01-02T15:04:05Z"),
|
||||
CreatedAt: sc.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
DateModified: sc.DateModified,
|
||||
}
|
||||
|
||||
|
||||
213
local/model/task.go
Normal file
213
local/model/task.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TaskStatus represents the status of a task
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
TaskStatusTodo TaskStatus = "todo"
|
||||
TaskStatusInProgress TaskStatus = "in_progress"
|
||||
TaskStatusDone TaskStatus = "done"
|
||||
TaskStatusCanceled TaskStatus = "canceled"
|
||||
)
|
||||
|
||||
// TaskPriority represents the priority of a task
|
||||
type TaskPriority string
|
||||
|
||||
const (
|
||||
TaskPriorityLow TaskPriority = "low"
|
||||
TaskPriorityMedium TaskPriority = "medium"
|
||||
TaskPriorityHigh TaskPriority = "high"
|
||||
)
|
||||
|
||||
// Task represents a task in the system
|
||||
type Task struct {
|
||||
BaseModel
|
||||
Title string `json:"title" gorm:"not null;type:varchar(255)"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Status TaskStatus `json:"status" gorm:"not null;default:'todo';type:varchar(20)"`
|
||||
Priority TaskPriority `json:"priority" gorm:"not null;default:'medium';type:varchar(20)"`
|
||||
ProjectID string `json:"project_id" gorm:"not null;type:uuid;index;references:projects(id);onDelete:CASCADE"`
|
||||
DueDate *string `json:"due_date" gorm:"type:date"`
|
||||
Project Project `json:"project,omitempty" gorm:"foreignKey:ProjectID"`
|
||||
Assignees []User `json:"assignees,omitempty" gorm:"many2many:task_assignees;"`
|
||||
}
|
||||
|
||||
// TaskCreateRequest represents the request to create a new task
|
||||
type TaskCreateRequest struct {
|
||||
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Status TaskStatus `json:"status" validate:"omitempty,oneof=todo in_progress done canceled"`
|
||||
Priority TaskPriority `json:"priority" validate:"omitempty,oneof=low medium high"`
|
||||
ProjectID string `json:"project_id" validate:"required,uuid"`
|
||||
DueDate *string `json:"due_date"`
|
||||
AssigneeIDs []string `json:"assignee_ids"`
|
||||
}
|
||||
|
||||
// TaskUpdateRequest represents the request to update a task
|
||||
type TaskUpdateRequest struct {
|
||||
Title *string `json:"title,omitempty" validate:"omitempty,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
||||
Status *TaskStatus `json:"status,omitempty" validate:"omitempty,oneof=todo in_progress done canceled"`
|
||||
Priority *TaskPriority `json:"priority,omitempty" validate:"omitempty,oneof=low medium high"`
|
||||
DueDate *string `json:"due_date,omitempty"`
|
||||
AssigneeIDs []string `json:"assignee_ids,omitempty"`
|
||||
}
|
||||
|
||||
// TaskInfo represents public task information
|
||||
type TaskInfo struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status TaskStatus `json:"status"`
|
||||
Priority TaskPriority `json:"priority"`
|
||||
ProjectID string `json:"project_id"`
|
||||
DueDate *string `json:"due_date"`
|
||||
Project ProjectInfo `json:"project,omitempty"`
|
||||
Assignees []UserInfo `json:"assignees,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TaskAssignee represents the many-to-many relationship between tasks and users
|
||||
type TaskAssignee struct {
|
||||
TaskID string `json:"task_id" gorm:"type:uuid;primaryKey"`
|
||||
UserID string `json:"user_id" gorm:"type:uuid;primaryKey"`
|
||||
Task Task `json:"task,omitempty" gorm:"foreignKey:TaskID"`
|
||||
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a task
|
||||
func (t *Task) BeforeCreate(tx *gorm.DB) error {
|
||||
t.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize title and description
|
||||
t.Title = strings.TrimSpace(t.Title)
|
||||
t.Description = strings.TrimSpace(t.Description)
|
||||
|
||||
// Set default values
|
||||
if t.Status == "" {
|
||||
t.Status = TaskStatusTodo
|
||||
}
|
||||
if t.Priority == "" {
|
||||
t.Priority = TaskPriorityMedium
|
||||
}
|
||||
|
||||
return t.Validate()
|
||||
}
|
||||
|
||||
// BeforeUpdate is called before updating a task
|
||||
func (t *Task) BeforeUpdate(tx *gorm.DB) error {
|
||||
t.BaseModel.BeforeUpdate()
|
||||
|
||||
// Normalize fields if they're being updated
|
||||
if t.Title != "" {
|
||||
t.Title = strings.TrimSpace(t.Title)
|
||||
}
|
||||
if t.Description != "" {
|
||||
t.Description = strings.TrimSpace(t.Description)
|
||||
}
|
||||
|
||||
return t.Validate()
|
||||
}
|
||||
|
||||
// Validate validates task data
|
||||
func (t *Task) Validate() error {
|
||||
if t.Title == "" {
|
||||
return errors.New("title is required")
|
||||
}
|
||||
|
||||
if len(t.Title) > 255 {
|
||||
return errors.New("title must not exceed 255 characters")
|
||||
}
|
||||
|
||||
if t.ProjectID == "" {
|
||||
return errors.New("project_id is required")
|
||||
}
|
||||
|
||||
// Validate status
|
||||
if t.Status != "" {
|
||||
switch t.Status {
|
||||
case TaskStatusTodo, TaskStatusInProgress, TaskStatusDone, TaskStatusCanceled:
|
||||
// Valid status
|
||||
default:
|
||||
return errors.New("invalid status")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate priority
|
||||
if t.Priority != "" {
|
||||
switch t.Priority {
|
||||
case TaskPriorityLow, TaskPriorityMedium, TaskPriorityHigh:
|
||||
// Valid priority
|
||||
default:
|
||||
return errors.New("invalid priority")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToTaskInfo converts Task to TaskInfo (public information)
|
||||
func (t *Task) ToTaskInfo() TaskInfo {
|
||||
taskInfo := TaskInfo{
|
||||
ID: t.ID,
|
||||
Title: t.Title,
|
||||
Description: t.Description,
|
||||
Status: t.Status,
|
||||
Priority: t.Priority,
|
||||
ProjectID: t.ProjectID,
|
||||
DueDate: t.DueDate,
|
||||
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
Assignees: make([]UserInfo, len(t.Assignees)),
|
||||
}
|
||||
|
||||
// Add project info if loaded
|
||||
if t.Project.ID != "" {
|
||||
taskInfo.Project = t.Project.ToProjectInfo()
|
||||
}
|
||||
|
||||
// Add assignee info if loaded
|
||||
for i, assignee := range t.Assignees {
|
||||
taskInfo.Assignees[i] = assignee.ToUserInfo()
|
||||
}
|
||||
|
||||
return taskInfo
|
||||
}
|
||||
|
||||
// IsAssignedTo checks if a user is assigned to this task
|
||||
func (t *Task) IsAssignedTo(userID string) bool {
|
||||
for _, assignee := range t.Assignees {
|
||||
if assignee.ID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCompleted checks if the task is completed
|
||||
func (t *Task) IsCompleted() bool {
|
||||
return t.Status == TaskStatusDone
|
||||
}
|
||||
|
||||
// IsCanceled checks if the task is canceled
|
||||
func (t *Task) IsCanceled() bool {
|
||||
return t.Status == TaskStatusCanceled
|
||||
}
|
||||
|
||||
// IsInProgress checks if the task is in progress
|
||||
func (t *Task) IsInProgress() bool {
|
||||
return t.Status == TaskStatusInProgress
|
||||
}
|
||||
|
||||
// IsTodo checks if the task is todo
|
||||
func (t *Task) IsTodo() bool {
|
||||
return t.Status == TaskStatusTodo
|
||||
}
|
||||
95
local/model/type.go
Normal file
95
local/model/type.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Type represents a project type in the system
|
||||
type Type struct {
|
||||
BaseModel
|
||||
UserID *string `json:"user_id" gorm:"type:uuid;index;references:users(id);onDelete:SET NULL"`
|
||||
Name string `json:"name" gorm:"not null;type:varchar(100)"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// TypeCreateRequest represents the request to create a new type
|
||||
type TypeCreateRequest struct {
|
||||
UserID *string `json:"user_id"`
|
||||
Name string `json:"name" validate:"required,min=1,max=100"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
}
|
||||
|
||||
// TypeUpdateRequest represents the request to update a type
|
||||
type TypeUpdateRequest struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=100"`
|
||||
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
|
||||
}
|
||||
|
||||
// TypeInfo represents public type information
|
||||
type TypeInfo struct {
|
||||
ID string `json:"id"`
|
||||
UserID *string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate is called before creating a type
|
||||
func (t *Type) BeforeCreate(tx *gorm.DB) error {
|
||||
t.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize name
|
||||
t.Name = strings.TrimSpace(t.Name)
|
||||
t.Description = strings.TrimSpace(t.Description)
|
||||
|
||||
return t.Validate()
|
||||
}
|
||||
|
||||
// BeforeUpdate is called before updating a type
|
||||
func (t *Type) BeforeUpdate(tx *gorm.DB) error {
|
||||
t.BaseModel.BeforeUpdate()
|
||||
|
||||
// Normalize fields if they're being updated
|
||||
if t.Name != "" {
|
||||
t.Name = strings.TrimSpace(t.Name)
|
||||
}
|
||||
if t.Description != "" {
|
||||
t.Description = strings.TrimSpace(t.Description)
|
||||
}
|
||||
|
||||
return t.Validate()
|
||||
}
|
||||
|
||||
// Validate validates type data
|
||||
func (t *Type) Validate() error {
|
||||
if t.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
if len(t.Name) > 100 {
|
||||
return errors.New("name must not exceed 100 characters")
|
||||
}
|
||||
|
||||
if len(t.Description) > 1000 {
|
||||
return errors.New("description must not exceed 1000 characters")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToTypeInfo converts Type to TypeInfo (public information)
|
||||
func (t *Type) ToTypeInfo() TypeInfo {
|
||||
return TypeInfo{
|
||||
ID: t.ID,
|
||||
UserID: t.UserID,
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: t.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
@@ -13,42 +13,98 @@ import (
|
||||
// User represents a user in the system
|
||||
type User struct {
|
||||
BaseModel
|
||||
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
|
||||
Username string `json:"username" gorm:"unique;not null;type:varchar(100)"`
|
||||
Name string `json:"name" gorm:"not null;type:varchar(255)"`
|
||||
PasswordHash string `json:"-" gorm:"not null;type:text"`
|
||||
Active bool `json:"active" gorm:"default:true"`
|
||||
EmailVerified bool `json:"emailVerified" gorm:"default:false"`
|
||||
EmailVerificationToken string `json:"-" gorm:"type:varchar(255)"`
|
||||
PasswordResetToken string `json:"-" gorm:"type:varchar(255)"`
|
||||
PasswordResetExpires *time.Time `json:"-"`
|
||||
LastLogin *time.Time `json:"lastLogin"`
|
||||
LoginAttempts int `json:"-" gorm:"default:0"`
|
||||
LockedUntil *time.Time `json:"-"`
|
||||
TwoFactorEnabled bool `json:"twoFactorEnabled" gorm:"default:false"`
|
||||
TwoFactorSecret string `json:"-" gorm:"type:varchar(255)"`
|
||||
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
|
||||
AuditLogs []AuditLog `json:"-" gorm:"foreignKey:UserID"`
|
||||
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
|
||||
PasswordHash string `json:"-" gorm:"not null;type:varchar(255)"`
|
||||
FullName string `json:"full_name" gorm:"type:varchar(255)"`
|
||||
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
|
||||
}
|
||||
|
||||
// UserCreateRequest represents the request to create a new user
|
||||
type UserCreateRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,min=3,max=50"`
|
||||
Name string `json:"name" validate:"required,min=2,max=100"`
|
||||
FullName string `json:"full_name" validate:"required,min=2,max=100"`
|
||||
Password string `json:"password" validate:"required,min=8"`
|
||||
RoleIDs []string `json:"roleIds"`
|
||||
}
|
||||
|
||||
// ToUser converts UserCreateRequest to User domain model
|
||||
func (req *UserCreateRequest) ToUser() (*User, error) {
|
||||
user := &User{
|
||||
Email: req.Email,
|
||||
FullName: req.FullName,
|
||||
}
|
||||
|
||||
// Handle password hashing
|
||||
if err := user.SetPassword(req.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Note: Roles will be set by the service layer after validation
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Validate validates the UserCreateRequest
|
||||
func (req *UserCreateRequest) Validate() error {
|
||||
if req.Email == "" {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
if !isValidEmail(req.Email) {
|
||||
return errors.New("invalid email format")
|
||||
}
|
||||
if len(req.Password) < 8 {
|
||||
return errors.New("password must be at least 8 characters")
|
||||
}
|
||||
if req.FullName == "" {
|
||||
return errors.New("full name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserUpdateRequest represents the request to update a user
|
||||
type UserUpdateRequest struct {
|
||||
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
||||
Username *string `json:"username,omitempty" validate:"omitempty,min=3,max=50"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
FullName *string `json:"full_name,omitempty" validate:"omitempty,min=2,max=100"`
|
||||
RoleIDs []string `json:"roleIds,omitempty"`
|
||||
}
|
||||
|
||||
// ApplyToUser applies the UserUpdateRequest to an existing User
|
||||
func (req *UserUpdateRequest) ApplyToUser(user *User) error {
|
||||
if req.Email != nil {
|
||||
user.Email = *req.Email
|
||||
}
|
||||
|
||||
if req.FullName != nil {
|
||||
user.FullName = *req.FullName
|
||||
}
|
||||
|
||||
// Note: Roles will be handled by the service layer
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the UserUpdateRequest
|
||||
func (req *UserUpdateRequest) Validate() error {
|
||||
if req.Email != nil && !isValidEmail(*req.Email) {
|
||||
return errors.New("invalid email format")
|
||||
}
|
||||
if req.FullName != nil && len(*req.FullName) == 0 {
|
||||
return errors.New("full name cannot be empty")
|
||||
}
|
||||
if req.FullName != nil && len(*req.FullName) > 255 {
|
||||
return errors.New("full name must not exceed 255 characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserResponse represents the response when returning user data
|
||||
type UserResponse struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
Roles []string `json:"roles"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// UserLoginRequest represents a login request
|
||||
type UserLoginRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
@@ -65,16 +121,13 @@ type UserLoginResponse struct {
|
||||
|
||||
// UserInfo represents public user information
|
||||
type UserInfo struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
LastLogin *time.Time `json:"lastLogin"`
|
||||
Roles []RoleInfo `json:"roles"`
|
||||
Permissions []string `json:"permissions"`
|
||||
DateCreated time.Time `json:"dateCreated"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"full_name"`
|
||||
Roles []RoleInfo `json:"roles"`
|
||||
Permissions []string `json:"permissions"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest represents a password change request
|
||||
@@ -98,10 +151,9 @@ type ResetPasswordConfirmRequest struct {
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
u.BaseModel.BeforeCreate()
|
||||
|
||||
// Normalize email and username
|
||||
// Normalize email and full name
|
||||
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
||||
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
|
||||
u.Name = strings.TrimSpace(u.Name)
|
||||
u.FullName = strings.TrimSpace(u.FullName)
|
||||
|
||||
return u.Validate()
|
||||
}
|
||||
@@ -114,11 +166,8 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
||||
if u.Email != "" {
|
||||
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
|
||||
}
|
||||
if u.Username != "" {
|
||||
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
|
||||
}
|
||||
if u.Name != "" {
|
||||
u.Name = strings.TrimSpace(u.Name)
|
||||
if u.FullName != "" {
|
||||
u.FullName = strings.TrimSpace(u.FullName)
|
||||
}
|
||||
|
||||
return u.Validate()
|
||||
@@ -134,24 +183,8 @@ func (u *User) Validate() error {
|
||||
return errors.New("invalid email format")
|
||||
}
|
||||
|
||||
if u.Username == "" {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
|
||||
if len(u.Username) < 3 || len(u.Username) > 50 {
|
||||
return errors.New("username must be between 3 and 50 characters")
|
||||
}
|
||||
|
||||
if !isValidUsername(u.Username) {
|
||||
return errors.New("username can only contain letters, numbers, underscores, and hyphens")
|
||||
}
|
||||
|
||||
if u.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
if len(u.Name) < 2 || len(u.Name) > 100 {
|
||||
return errors.New("name must be between 2 and 100 characters")
|
||||
if u.FullName != "" && len(u.FullName) > 255 {
|
||||
return errors.New("full name must not exceed 255 characters")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -182,55 +215,16 @@ func (u *User) VerifyPassword(plainPassword string) bool {
|
||||
return u.CheckPassword(plainPassword)
|
||||
}
|
||||
|
||||
// IsLocked checks if the user account is locked
|
||||
func (u *User) IsLocked() bool {
|
||||
if u.LockedUntil == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().Before(*u.LockedUntil)
|
||||
}
|
||||
|
||||
// Lock locks the user account for the specified duration
|
||||
func (u *User) Lock(duration time.Duration) {
|
||||
lockUntil := time.Now().Add(duration)
|
||||
u.LockedUntil = &lockUntil
|
||||
}
|
||||
|
||||
// Unlock unlocks the user account
|
||||
func (u *User) Unlock() {
|
||||
u.LockedUntil = nil
|
||||
u.LoginAttempts = 0
|
||||
}
|
||||
|
||||
// IncrementLoginAttempts increments the login attempt counter
|
||||
func (u *User) IncrementLoginAttempts() {
|
||||
u.LoginAttempts++
|
||||
}
|
||||
|
||||
// ResetLoginAttempts resets the login attempt counter
|
||||
func (u *User) ResetLoginAttempts() {
|
||||
u.LoginAttempts = 0
|
||||
}
|
||||
|
||||
// UpdateLastLogin updates the last login timestamp
|
||||
func (u *User) UpdateLastLogin() {
|
||||
now := time.Now()
|
||||
u.LastLogin = &now
|
||||
}
|
||||
|
||||
// ToUserInfo converts User to UserInfo (public information)
|
||||
func (u *User) ToUserInfo() UserInfo {
|
||||
userInfo := UserInfo{
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
Username: u.Username,
|
||||
Name: u.Name,
|
||||
Active: u.Active,
|
||||
EmailVerified: u.EmailVerified,
|
||||
LastLogin: u.LastLogin,
|
||||
DateCreated: u.DateCreated,
|
||||
Roles: make([]RoleInfo, len(u.Roles)),
|
||||
Permissions: []string{},
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
FullName: u.FullName,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
Roles: make([]RoleInfo, len(u.Roles)),
|
||||
Permissions: []string{},
|
||||
}
|
||||
|
||||
// Convert roles and collect permissions
|
||||
@@ -250,6 +244,23 @@ func (u *User) ToUserInfo() UserInfo {
|
||||
return userInfo
|
||||
}
|
||||
|
||||
// ToResponse converts User to UserResponse (for API responses)
|
||||
func (u *User) ToResponse() *UserResponse {
|
||||
roleNames := make([]string, len(u.Roles))
|
||||
for i, role := range u.Roles {
|
||||
roleNames[i] = role.Name
|
||||
}
|
||||
|
||||
return &UserResponse{
|
||||
ID: u.ID,
|
||||
Email: u.Email,
|
||||
FullName: u.FullName,
|
||||
Roles: roleNames,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// HasRole checks if the user has a specific role
|
||||
func (u *User) HasRole(roleName string) bool {
|
||||
for _, role := range u.Roles {
|
||||
@@ -278,12 +289,6 @@ func isValidEmail(email string) bool {
|
||||
return emailRegex.MatchString(email)
|
||||
}
|
||||
|
||||
// isValidUsername validates username format
|
||||
func isValidUsername(username string) bool {
|
||||
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
|
||||
return usernameRegex.MatchString(username)
|
||||
}
|
||||
|
||||
// validatePassword validates password strength
|
||||
func validatePassword(password string) error {
|
||||
if len(password) < 8 {
|
||||
@@ -294,25 +299,5 @@ func validatePassword(password string) error {
|
||||
return errors.New("password must not exceed 128 characters")
|
||||
}
|
||||
|
||||
// Check for at least one lowercase letter
|
||||
if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
|
||||
return errors.New("password must contain at least one lowercase letter")
|
||||
}
|
||||
|
||||
// Check for at least one uppercase letter
|
||||
if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
|
||||
return errors.New("password must contain at least one uppercase letter")
|
||||
}
|
||||
|
||||
// Check for at least one digit
|
||||
if matched, _ := regexp.MatchString(`\d`, password); !matched {
|
||||
return errors.New("password must contain at least one digit")
|
||||
}
|
||||
|
||||
// Check for at least one special character
|
||||
if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
|
||||
return errors.New("password must contain at least one special character")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ func NewMembershipRepository(db *gorm.DB) *MembershipRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// FindUserByUsername finds a user by their username.
|
||||
// FindUserByEmail finds a user by their email.
|
||||
// It preloads the user's role and the role's permissions.
|
||||
func (r *MembershipRepository) FindUserByUsername(ctx context.Context, username string) (*model.User, error) {
|
||||
func (r *MembershipRepository) FindUserByEmail(ctx context.Context, email string) (*model.User, error) {
|
||||
var user model.User
|
||||
db := r.db.WithContext(ctx)
|
||||
err := db.Preload("Roles.Permissions").Where("username = ?", username).First(&user).Error
|
||||
err := db.Preload("Roles.Permissions").Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"omega-server/local/utl/jwt"
|
||||
"omega-server/local/utl/logging"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -38,8 +39,8 @@ func (s *MembershipService) SetCacheInvalidator(invalidator CacheInvalidator) {
|
||||
}
|
||||
|
||||
// Login authenticates a user and returns a JWT.
|
||||
func (s *MembershipService) Login(ctx context.Context, username, password string) (string, error) {
|
||||
user, err := s.repo.FindUserByUsername(ctx, username)
|
||||
func (s *MembershipService) Login(ctx context.Context, email, password string) (string, error) {
|
||||
user, err := s.repo.FindUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return "", errors.New("invalid credentials")
|
||||
}
|
||||
@@ -55,38 +56,42 @@ func (s *MembershipService) Login(ctx context.Context, username, password string
|
||||
roleNames[i] = role.Name
|
||||
}
|
||||
|
||||
return jwt.GenerateToken(user.ID, user.Email, user.Username, roleNames)
|
||||
return jwt.GenerateToken(user.ID, user.Email, user.FullName, roleNames)
|
||||
}
|
||||
|
||||
// CreateUser creates a new user.
|
||||
func (s *MembershipService) CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error) {
|
||||
|
||||
role, err := s.repo.FindRoleByName(ctx, roleName)
|
||||
if err != nil {
|
||||
logging.Error("Failed to find role by name: %v", err)
|
||||
return nil, errors.New("role not found")
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
Username: username,
|
||||
Email: username + "@example.com", // You may want to accept email as parameter
|
||||
Name: username,
|
||||
}
|
||||
|
||||
// Set password using the model's SetPassword method
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
func (s *MembershipService) CreateUser(ctx context.Context, user *model.User, roleIDs []string) (*model.User, error) {
|
||||
// Validate domain model
|
||||
if err := user.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Assign roles
|
||||
user.Roles = []model.Role{*role}
|
||||
// Handle roles
|
||||
if len(roleIDs) > 0 {
|
||||
roles := make([]model.Role, 0, len(roleIDs))
|
||||
for _, roleID := range roleIDs {
|
||||
role, err := s.repo.FindRoleByName(ctx, roleID)
|
||||
if err != nil {
|
||||
logging.Error("Failed to find role by name: %v", err)
|
||||
return nil, errors.New("role not found: " + roleID)
|
||||
}
|
||||
roles = append(roles, *role)
|
||||
}
|
||||
user.Roles = roles
|
||||
}
|
||||
|
||||
// Create user
|
||||
if err := s.repo.CreateUser(ctx, user); err != nil {
|
||||
logging.Error("Failed to create user: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.InfoOperation("USER_CREATE", "Created user: "+user.Username+" (ID: "+user.ID+", Role: "+roleName+")")
|
||||
// Log with role names
|
||||
roleNames := make([]string, len(user.Roles))
|
||||
for i, role := range user.Roles {
|
||||
roleNames[i] = role.Name
|
||||
}
|
||||
logging.InfoOperation("USER_CREATE", "Created user: "+user.Email+" (ID: "+user.ID+", Roles: "+strings.Join(roleNames, ", ")+")")
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -105,13 +110,6 @@ func (s *MembershipService) GetUserWithPermissions(ctx context.Context, userID s
|
||||
return s.repo.FindUserByIDWithPermissions(ctx, userID)
|
||||
}
|
||||
|
||||
// UpdateUserRequest defines the request body for updating a user.
|
||||
type UpdateUserRequest struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
RoleID *string `json:"roleId"`
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user with validation to prevent Super Admin deletion.
|
||||
func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
|
||||
// Get user with role information
|
||||
@@ -142,34 +140,42 @@ func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) er
|
||||
}
|
||||
|
||||
// UpdateUser updates a user's details.
|
||||
func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error) {
|
||||
func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req *model.UserUpdateRequest) (*model.User, error) {
|
||||
// Validate request
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := s.repo.FindUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
if req.Username != nil {
|
||||
user.Username = *req.Username
|
||||
// Apply update request to user
|
||||
if err := req.ApplyToUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Password != nil && *req.Password != "" {
|
||||
// Use the model's SetPassword method to hash the password
|
||||
if err := user.SetPassword(*req.Password); err != nil {
|
||||
return nil, err
|
||||
// Handle roles if provided
|
||||
if len(req.RoleIDs) > 0 {
|
||||
roles := make([]model.Role, 0, len(req.RoleIDs))
|
||||
for _, roleID := range req.RoleIDs {
|
||||
roleUUID, err := uuid.Parse(roleID)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid role ID format: " + roleID)
|
||||
}
|
||||
role, err := s.repo.FindRoleByID(ctx, roleUUID)
|
||||
if err != nil {
|
||||
return nil, errors.New("role not found: " + roleID)
|
||||
}
|
||||
roles = append(roles, *role)
|
||||
}
|
||||
user.Roles = roles
|
||||
}
|
||||
|
||||
if req.RoleID != nil {
|
||||
// Check if role exists
|
||||
roleUUID, err := uuid.Parse(*req.RoleID)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid role ID format")
|
||||
}
|
||||
role, err := s.repo.FindRoleByID(ctx, roleUUID)
|
||||
if err != nil {
|
||||
return nil, errors.New("role not found")
|
||||
}
|
||||
user.Roles = []model.Role{*role}
|
||||
// Validate updated user
|
||||
if err := user.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.repo.UpdateUser(ctx, user); err != nil {
|
||||
@@ -177,11 +183,11 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re
|
||||
}
|
||||
|
||||
// Invalidate cache if role was changed
|
||||
if req.RoleID != nil && s.cacheInvalidator != nil {
|
||||
if len(req.RoleIDs) > 0 && s.cacheInvalidator != nil {
|
||||
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
|
||||
}
|
||||
|
||||
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Username+" (ID: "+user.ID+")")
|
||||
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Email+" (ID: "+user.ID+")")
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -289,10 +295,17 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Create a default admin user if one doesn't exist
|
||||
_, err = s.repo.FindUserByUsername(ctx, "admin")
|
||||
_, err = s.repo.FindUserByEmail(ctx, "admin@example.com")
|
||||
if err != nil {
|
||||
logging.Debug("Creating default admin user")
|
||||
_, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed
|
||||
adminUser := &model.User{
|
||||
Email: "admin@example.com",
|
||||
FullName: "System Administrator",
|
||||
}
|
||||
if err := adminUser.SetPassword(os.Getenv("PASSWORD")); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.CreateUser(ctx, adminUser, []string{"Super Admin"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"omega-server/local/model"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MembershipServiceInterface defines the interface for membership-related operations
|
||||
type MembershipServiceInterface interface {
|
||||
// Authentication and Authorization
|
||||
Login(ctx context.Context, username, password string) (string, error)
|
||||
HasPermission(ctx context.Context, userID string, permissionName string) (bool, error)
|
||||
GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error)
|
||||
SetCacheInvalidator(invalidator CacheInvalidator)
|
||||
|
||||
// User Management
|
||||
CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error)
|
||||
ListUsers(ctx context.Context) ([]*model.User, error)
|
||||
GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error)
|
||||
DeleteUser(ctx context.Context, userID uuid.UUID) error
|
||||
UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error)
|
||||
|
||||
// Role Management
|
||||
GetAllRoles(ctx context.Context) ([]*model.Role, error)
|
||||
SetupInitialData(ctx context.Context) error
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func DefaultPagination() PaginationRequest {
|
||||
return PaginationRequest{
|
||||
Page: 1,
|
||||
Limit: 10,
|
||||
Sort: "dateCreated",
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (p *PaginationRequest) Validate() {
|
||||
p.Limit = 10
|
||||
}
|
||||
if p.Sort == "" {
|
||||
p.Sort = "dateCreated"
|
||||
p.Sort = "created_at"
|
||||
}
|
||||
if p.Order != "asc" && p.Order != "desc" {
|
||||
p.Order = "desc"
|
||||
|
||||
@@ -1,23 +1,53 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"omega-server/local/model"
|
||||
"omega-server/local/utl/logging"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.uber.org/dig"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func Start(di *dig.Container) {
|
||||
// PostgreSQL connection configuration
|
||||
host := os.Getenv("DB_HOST")
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
port := os.Getenv("DB_PORT")
|
||||
if port == "" {
|
||||
port = "5432"
|
||||
}
|
||||
|
||||
user := os.Getenv("DB_USER")
|
||||
if user == "" {
|
||||
user = "postgres"
|
||||
}
|
||||
|
||||
password := os.Getenv("DB_PASSWORD")
|
||||
if password == "" {
|
||||
password = "password"
|
||||
}
|
||||
|
||||
dbName := os.Getenv("DB_NAME")
|
||||
if dbName == "" {
|
||||
dbName = "app.db"
|
||||
dbName = "omega_db"
|
||||
}
|
||||
|
||||
sslMode := os.Getenv("DB_SSL_MODE")
|
||||
if sslMode == "" {
|
||||
sslMode = "disable"
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=UTC",
|
||||
host, user, password, dbName, port, sslMode)
|
||||
|
||||
// Configure GORM logger
|
||||
gormLogger := logger.Default
|
||||
if os.Getenv("LOG_LEVEL") == "DEBUG" {
|
||||
@@ -26,10 +56,14 @@ func Start(di *dig.Container) {
|
||||
gormLogger = logger.Default.LogMode(logger.Silent)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: gormLogger,
|
||||
})
|
||||
if err != nil {
|
||||
logging.Error("Failed to connect to PostgreSQL database")
|
||||
logging.Error("Connection string: host=%s user=%s dbname=%s port=%s sslmode=%s", host, user, dbName, port, sslMode)
|
||||
logging.Error("Error: %v", err)
|
||||
logging.Error("Make sure PostgreSQL is running and the database exists")
|
||||
logging.Panic("failed to connect database: " + err.Error())
|
||||
}
|
||||
|
||||
@@ -62,6 +96,12 @@ func Migrate(db *gorm.DB) {
|
||||
&model.User{},
|
||||
&model.Role{},
|
||||
&model.Permission{},
|
||||
&model.Type{},
|
||||
&model.Project{},
|
||||
&model.Task{},
|
||||
&model.Integration{},
|
||||
&model.ProjectMember{},
|
||||
&model.TaskAssignee{},
|
||||
&model.SystemConfig{},
|
||||
&model.AuditLog{},
|
||||
&model.SecurityEvent{},
|
||||
@@ -87,6 +127,9 @@ func Seed(db *gorm.DB) error {
|
||||
if err := seedDefaultAdmin(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedDefaultTypes(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedSystemConfigs(db); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -193,6 +236,31 @@ func seedPermissions(db *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedDefaultTypes(db *gorm.DB) error {
|
||||
defaultTypes := []model.Type{
|
||||
{Name: "Web Development", Description: "Standard web development project"},
|
||||
{Name: "Mobile App", Description: "Mobile application development"},
|
||||
{Name: "API Development", Description: "API and backend service development"},
|
||||
{Name: "Data Science", Description: "Data analysis and machine learning projects"},
|
||||
{Name: "DevOps", Description: "Infrastructure and deployment projects"},
|
||||
{Name: "Research", Description: "Research and documentation projects"},
|
||||
}
|
||||
|
||||
for _, projectType := range defaultTypes {
|
||||
var existingType model.Type
|
||||
err := db.Where("name = ? AND user_id IS NULL", projectType.Name).First(&existingType).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
projectType.Init()
|
||||
if err := db.Create(&projectType).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
logging.Info("Created default project type: %s", projectType.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedDefaultAdmin(db *gorm.DB) error {
|
||||
// Check if admin user already exists
|
||||
var existingAdmin model.User
|
||||
@@ -214,10 +282,9 @@ func seedDefaultAdmin(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
admin := model.User{
|
||||
Email: "admin@example.com",
|
||||
Username: "admin",
|
||||
Name: "System Administrator",
|
||||
Active: true,
|
||||
Email: "admin@example.com",
|
||||
FullName: "System Administrator",
|
||||
PasswordHash: "",
|
||||
}
|
||||
admin.Init()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user