diff --git a/Internal/adapters/repository/postgres.go b/Internal/adapters/repository/postgres.go index cbf2b20..a6a2f28 100644 --- a/Internal/adapters/repository/postgres.go +++ b/Internal/adapters/repository/postgres.go @@ -42,6 +42,7 @@ func (db *DBContainer) Migrate() error { &domain.User{}, &domain.Invitation{}, &domain.Project{}, + &domain.ProjectUser{}, ) if err != nil { log.Fatalf("Failed to migrate users: %v", err) diff --git a/Internal/adapters/repository/projectRepo.go b/Internal/adapters/repository/projectRepo.go index 8ede3b1..15d8a51 100644 --- a/Internal/adapters/repository/projectRepo.go +++ b/Internal/adapters/repository/projectRepo.go @@ -28,8 +28,31 @@ func (pr *ProjectRepo) FetchProjects(projects []domain.Project) ([]domain.Projec func (pr *ProjectRepo) ProjectById(id string) (*domain.Project, error) { var project domain.Project - if err := pr.DB.Gorm.First(&project, id).Error; err != nil { + if err := pr.DB.Gorm.Preload("ProjectUsers.User").First(&project, id).Error; err != nil { return nil, err } return &project, nil } + +func (pr *ProjectRepo) InviteProject(projectuser *domain.ProjectUser) (*domain.ProjectUser, error) { + if err := pr.DB.Gorm.Create(projectuser).Error; err != nil { + return nil, err + } + return projectuser, nil +} + +func (pr *ProjectRepo) ProjectUserCheck(userID uint) (*domain.ProjectUser, error) { + var projectuser domain.ProjectUser + if err := pr.DB.Gorm.First(&projectuser, userID).Error; err != nil { + return nil, err + } + return &projectuser, nil +} + +func (pr *ProjectRepo) RemoveProjectUser(id uint) error { + var projectuser domain.ProjectUser + if err := pr.DB.Gorm.Delete(&projectuser, id).Error; err != nil { + return err + } + return nil +} diff --git a/Internal/adapters/repository/userRepo.go b/Internal/adapters/repository/userRepo.go index e965fbd..0015945 100644 --- a/Internal/adapters/repository/userRepo.go +++ b/Internal/adapters/repository/userRepo.go @@ -17,12 +17,21 @@ func NewUserRepository(db *DBContainer) *UserRepository { // Query func (ur *UserRepository) save(user *domain.User) error { + return ur.db.Gorm.Create(user).Error } func (ur *UserRepository) FindById(id uint) (*domain.User, error) { var user domain.User - err := ur.db.Gorm.First(&user, id).Error + if err := ur.db.Gorm.Preload("Projects.Project").First(&user, id).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (ur *UserRepository) FindByEmail(email string) (*domain.User, error) { + var user domain.User + err := ur.db.Gorm.Where("email = ?", email).First(&user).Error if err != nil { return nil, err } diff --git a/Internal/api/Routes/routev1.go b/Internal/api/Routes/routev1.go index c908a3e..03336d5 100644 --- a/Internal/api/Routes/routev1.go +++ b/Internal/api/Routes/routev1.go @@ -52,6 +52,12 @@ func SetupRouterV1(r *gin.Engine, deps Deps) { { project.GET("/projects", deps.Project.GetAllProjectHandler) project.GET("/:projectid", deps.Project.GetProjectHandler) + + manage := project.Group("/manage") + { + manage.POST("/invite/:projectid", deps.Project.InviteProjectHandler) + manage.DELETE("/remove/:projuserid", deps.Project.RemoveProjectUserHandler) + } } } } diff --git a/Internal/api/handlers/projectHandler.go b/Internal/api/handlers/projectHandler.go index 043041d..e302d32 100644 --- a/Internal/api/handlers/projectHandler.go +++ b/Internal/api/handlers/projectHandler.go @@ -1,6 +1,8 @@ package handlers import ( + "strconv" + "github.com/Arjuna-Ragil/Localbase/Internal/core/services" "github.com/gin-gonic/gin" ) @@ -77,3 +79,62 @@ func (ph *ProjectHandler) GetProjectHandler(c *gin.Context) { "data": project, }) } + +func (ph *ProjectHandler) InviteProjectHandler(c *gin.Context) { + projectIDStr := c.Param("projectid") + projectID, err := strconv.Atoi(projectIDStr) + if err != nil { + c.JSON(400, gin.H{ + "message": "Invalid Project ID", + "error": err.Error(), + }) + return + } + userRole := c.GetString("userRole") + userID := c.GetUint("userId") + var input services.InviteProjectInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(400, gin.H{ + "message": "Invalid input", + "data": err.Error(), + }) + return + } + invite, err := ph.ProjectService.InviteProject(&input, userRole, userID, uint(projectID)) + if err != nil { + c.JSON(500, gin.H{ + "message": "failed to invite project", + "data": err.Error(), + }) + return + } + c.JSON(200, gin.H{ + "message": "successfully invited project", + "data": invite, + }) +} + +func (ph *ProjectHandler) RemoveProjectUserHandler(c *gin.Context) { + projectUserSTR := c.Param("projuserid") + projectUserID, err := strconv.Atoi(projectUserSTR) + if err != nil { + c.JSON(400, gin.H{ + "message": "Invalid Project ID", + "error": err.Error(), + }) + return + } + userRole := c.GetString("userRole") + userID := c.GetUint("userId") + if err := ph.ProjectService.DeleteProjectUser(uint(projectUserID), userRole, userID); err != nil { + c.JSON(500, gin.H{ + "message": "failed to remove project user", + "data": err.Error(), + }) + return + } + c.JSON(200, gin.H{ + "message": "successfully removed project user", + "data": nil, + }) +} diff --git a/Internal/api/middleware/auth.go b/Internal/api/middleware/auth.go index 8c6ce6b..e77d2e4 100644 --- a/Internal/api/middleware/auth.go +++ b/Internal/api/middleware/auth.go @@ -71,7 +71,7 @@ func AuthMiddleware(userRepo *repository.UserRepository, cfg *config.Config) gin return } - c.Set("userID", user.Id) + c.Set("userID", user.ID) c.Set("userRole", user.Role) c.Next() diff --git a/Internal/core/domain/projectdb.go b/Internal/core/domain/projectdb.go index 031512a..cd6be9e 100644 --- a/Internal/core/domain/projectdb.go +++ b/Internal/core/domain/projectdb.go @@ -3,9 +3,22 @@ package domain import "time" type Project struct { - ID uint `gorm:"primary_key;auto_increment" json:"id"` - Name string `gorm:"size:255;not null" json:"name"` - Desc string `gorm:"size:255;" json:"desc"` - AdminID uint `json:"admin_id"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Name string `gorm:"size:255;not null" json:"name"` + Desc string `gorm:"size:255;" json:"desc"` + AdminID uint `json:"admin_id"` + ProjectUsers []ProjectUser `json:"project_users,omitempty"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` +} + +type ProjectUser struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + ProjectID uint `json:"project_id"` + UserID uint `json:"user_id"` + Project *Project `json:"project,omitempty"` + User *User `json:"user,omitempty"` + Role string `gorm:"size:255;not null" json:"role"` CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` } + +//Changes last savepoint diff --git a/Internal/core/domain/userdb.go b/Internal/core/domain/userdb.go index 110598d..e117373 100644 --- a/Internal/core/domain/userdb.go +++ b/Internal/core/domain/userdb.go @@ -8,15 +8,18 @@ import ( ) type User struct { - Id uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` - Email string `gorm:"size:255;unique;not null" json:"email"` - Password string `json:"-"` - Username string `gorm:"size:255;unique;not null" json:"username"` - Role string `gorm:"size:255;not null" json:"role"` - CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` - UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Email string `gorm:"size:255;unique;not null" json:"email"` + Password string `json:"-"` + Username string `gorm:"size:255;unique;not null" json:"username"` + Role string `gorm:"size:255;not null" json:"role"` + Projects []ProjectUser `json:"projects"` + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` + UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` } +//last chnages savepoint + func (u *User) BeforeCreate(tx *gorm.DB) (err error) { if len(u.Password) < 50 { hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) diff --git a/Internal/core/services/authService.go b/Internal/core/services/authService.go index 8c7145f..17506d2 100644 --- a/Internal/core/services/authService.go +++ b/Internal/core/services/authService.go @@ -95,7 +95,7 @@ func (as *AuthService) LoginService(input *LoginInput) (string, error) { return "", err } token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": user.Id, + "sub": user.ID, "exp": time.Now().Add(time.Hour * 48).Unix(), }) tokenString, err := token.SignedString([]byte(cfg.SecretKey)) diff --git a/Internal/core/services/projectService.go b/Internal/core/services/projectService.go index 8d36158..aec2a14 100644 --- a/Internal/core/services/projectService.go +++ b/Internal/core/services/projectService.go @@ -9,10 +9,11 @@ import ( type ProjectService struct { ProjectRepo *repository.ProjectRepo + UserRepo *repository.UserRepository } -func NewProjectService(projectRepo *repository.ProjectRepo) *ProjectService { - return &ProjectService{ProjectRepo: projectRepo} +func NewProjectService(project *repository.ProjectRepo, user *repository.UserRepository) *ProjectService { + return &ProjectService{ProjectRepo: project, UserRepo: user} } type CreateInput struct { @@ -21,6 +22,11 @@ type CreateInput struct { AdminID uint `json:"admin_id"` } +type InviteProjectInput struct { + Email string `json:"email"` + Role string `json:"role"` +} + func (ps *ProjectService) CreateProject(input *CreateInput) (*domain.Project, error) { projectInfo := domain.Project{ Name: input.Name, @@ -53,3 +59,45 @@ func (ps *ProjectService) GetProjectById(id string) (*domain.Project, error) { } return project, nil } + +func (ps *ProjectService) InviteProject(input *InviteProjectInput, Role string, userID uint, projectID uint) (*domain.ProjectUser, error) { + if Role != "admin" { + prorole, err := ps.ProjectRepo.ProjectUserCheck(userID) + if err != nil { + return nil, errors.New("user is not part of project") + } + if prorole.Role != "lead" { + return nil, errors.New("permission denied") + } + } + user, err := ps.UserRepo.FindByEmail(input.Email) + if err != nil { + return nil, err + } + var inviteInfo = domain.ProjectUser{ + ProjectID: projectID, + UserID: user.ID, + Role: input.Role, + } + project, err := ps.ProjectRepo.InviteProject(&inviteInfo) + if err != nil { + return nil, err + } + return project, nil +} + +func (ps *ProjectService) DeleteProjectUser(id uint, Role string, userID uint) error { + if Role != "admin" { + prorole, err := ps.ProjectRepo.ProjectUserCheck(userID) + if err != nil { + return errors.New("user is not part of project") + } + if prorole.Role != "lead" { + return errors.New("permission denied") + } + } + if err := ps.ProjectRepo.RemoveProjectUser(id); err != nil { + return err + } + return nil +} diff --git a/Lb-web/src/features/Login/context/AuthContext.tsx b/Lb-web/src/features/Login/context/AuthContext.tsx index cc84aca..e42c51d 100644 --- a/Lb-web/src/features/Login/context/AuthContext.tsx +++ b/Lb-web/src/features/Login/context/AuthContext.tsx @@ -6,6 +6,17 @@ interface User { username: string; email: string; role: string; + projects?: { + id: number; + project_id: number; + role: string; + project?: { + id: number; + name: string; + desc: string; + created_at: string; + }; + }[]; } interface AuthContextType { diff --git a/Lb-web/src/features/project/components/ProjectSidebar.tsx b/Lb-web/src/features/project/components/ProjectSidebar.tsx new file mode 100644 index 0000000..45916ea --- /dev/null +++ b/Lb-web/src/features/project/components/ProjectSidebar.tsx @@ -0,0 +1,69 @@ +import { Link, useLocation } from "react-router"; +import { Button } from "@/components/ui/button"; +import { LayoutDashboard, Settings, ArrowLeft, Database } from "lucide-react"; + +interface ProjectSidebarProps { + project: { + id: number; + name: string; + }; +} + +export function ProjectSidebar({ project }: ProjectSidebarProps) { + const location = useLocation(); + + const isActive = (path: string) => { + return location.pathname === path || location.pathname.startsWith(`${path}/`); + }; + + return ( + + ); +} diff --git a/Lb-web/src/features/project/pages/ProjectDashboard.tsx b/Lb-web/src/features/project/pages/ProjectDashboard.tsx index e61e152..19456e2 100644 --- a/Lb-web/src/features/project/pages/ProjectDashboard.tsx +++ b/Lb-web/src/features/project/pages/ProjectDashboard.tsx @@ -1,8 +1,9 @@ import { useEffect, useState } from "react"; import { useParams, Link } from "react-router"; import { projectService } from "../services/projectService"; +import { ProjectSidebar } from "../components/ProjectSidebar"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, LayoutDashboard, Settings } from "lucide-react"; +import { ArrowLeft } from "lucide-react"; export default function ProjectDashboard() { @@ -52,58 +53,44 @@ export default function ProjectDashboard() { } return ( -
Project Dashboard
+- {project.desc || "No description provided for this project."} -
-+ {project.desc || "No description provided for this project."} +
+Manage users who have access to this project.
++ Start by inviting users to collaborate on this project. +
+| Username | +Role | +Joined | +Actions | +|
|---|---|---|---|---|
| {user.username} | +{user.email} | ++ + {user.role} + + | ++ {new Date(user.joined_at).toLocaleDateString()} + | ++ + | +