Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Internal/adapters/repository/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 24 additions & 1 deletion Internal/adapters/repository/projectRepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
11 changes: 10 additions & 1 deletion Internal/adapters/repository/userRepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions Internal/api/Routes/routev1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions Internal/api/handlers/projectHandler.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package handlers

import (
"strconv"

"github.com/Arjuna-Ragil/Localbase/Internal/core/services"
"github.com/gin-gonic/gin"
)
Expand Down Expand Up @@ -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,
})
}
2 changes: 1 addition & 1 deletion Internal/api/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
21 changes: 17 additions & 4 deletions Internal/core/domain/projectdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 10 additions & 7 deletions Internal/core/domain/userdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Internal/core/services/authService.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
52 changes: 50 additions & 2 deletions Internal/core/services/projectService.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
11 changes: 11 additions & 0 deletions Lb-web/src/features/Login/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
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 {
Expand Down Expand Up @@ -68,7 +79,7 @@
);
}

export function useAuth() {

Check failure on line 82 in Lb-web/src/features/Login/context/AuthContext.tsx

View workflow job for this annotation

GitHub Actions / Frontend integrity checks

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
Expand Down
69 changes: 69 additions & 0 deletions Lb-web/src/features/project/components/ProjectSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<aside className="w-64 border-r border-sky-100 bg-white/50 backdrop-blur-md flex flex-col h-full z-20">
{/* Project Header */}
<div className="p-6 border-b border-sky-100">
<div className="flex items-center space-x-3 mb-1">
<div className="h-10 w-10 bg-sky-100 rounded-lg flex items-center justify-center text-sky-600">
<Database className="h-6 w-6" />
</div>
<div>
<h2 className="font-bold text-sky-900 truncate max-w-[140px]" title={project.name}>
{project.name}
</h2>
<span className="text-xs text-sky-500 font-medium">Project</span>
</div>
</div>
</div>

{/* Navigation */}
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
<Link to={`/project/${project.id}`}>
<Button
variant="ghost"
className={`w-full justify-start ${isActive(`/project/${project.id}`) && !location.pathname.includes('settings') ? 'bg-sky-100 text-sky-900' : 'text-sky-700 hover:text-sky-900 hover:bg-sky-50'}`}
>
<LayoutDashboard className="mr-3 h-4 w-4" />
Dashboard
</Button>
</Link>
<Link to={`/project/${project.id}/settings/members`}>
<Button
variant="ghost"
className={`w-full justify-start ${isActive(`/project/${project.id}/settings/members`) ? 'bg-sky-100 text-sky-900' : 'text-sky-700 hover:text-sky-900 hover:bg-sky-50'}`}
>
<Settings className="mr-3 h-4 w-4" />
Members
</Button>
</Link>
</nav>

{/* Footer */}
<div className="p-4 border-t border-sky-100">
<Link to="/">
<Button variant="ghost" className="w-full justify-start text-sky-600 hover:text-sky-800 hover:bg-sky-50">
<ArrowLeft className="mr-3 h-4 w-4" />
Back to Home
</Button>
</Link>
</div>
</aside>
);
}
Loading
Loading