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 @@ -41,6 +41,7 @@ func (db *DBContainer) Migrate() error {
err := db.Gorm.AutoMigrate(
&domain.User{},
&domain.Invitation{},
&domain.Project{},
)
if err != nil {
log.Fatalf("Failed to migrate users: %v", err)
Expand Down
27 changes: 27 additions & 0 deletions Internal/adapters/repository/projectRepo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package repository

import (
"github.com/Arjuna-Ragil/Localbase/Internal/core/domain"
)

type ProjectRepo struct {
DB *DBContainer
}

func NewProjectRepo(db *DBContainer) *ProjectRepo {
return &ProjectRepo{DB: db}
}

func (pr *ProjectRepo) CreateProject(project *domain.Project) (*domain.Project, error) {
if err := pr.DB.Gorm.Create(project).Error; err != nil {
return nil, err
}
return project, nil
}

func (pr *ProjectRepo) FetchProjects(projects []domain.Project) ([]domain.Project, error) {
if err := pr.DB.Gorm.Find(&projects).Error; err != nil {
return nil, err
}
return projects, nil
}
6 changes: 6 additions & 0 deletions Internal/api/Routes/routev1.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Deps struct {
User *handlers.UserHandler
Auth *handlers.AuthHandler
System *handlers.SystemHandler
Project *handlers.ProjectHandler
UserRepo *repository.UserRepository
Config *config.Config
}
Expand Down Expand Up @@ -45,6 +46,11 @@ func SetupRouterV1(r *gin.Engine, deps Deps) {
admin.POST("/invite", deps.Auth.CreateInviteHandler)
admin.GET("/alluser", deps.User.AllUserHandler)
admin.PUT("/updaterole", deps.User.UpdateRoleHandler)
admin.POST("/createproject", deps.Project.CreateProjectHandler)
}
project := protected.Group("/project")
{
project.GET("/projects", deps.Project.GetAllProjectHandler)
}
}
}
Expand Down
63 changes: 63 additions & 0 deletions Internal/api/handlers/projectHandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package handlers

import (
"github.com/Arjuna-Ragil/Localbase/Internal/core/services"
"github.com/gin-gonic/gin"
)

type ProjectHandler struct {
ProjectService *services.ProjectService
}

func NewProjectHandler(projectService *services.ProjectService) *ProjectHandler {
return &ProjectHandler{ProjectService: projectService}
}

func (ph *ProjectHandler) CreateProjectHandler(c *gin.Context) {
var input services.CreateInput
userRole, _ := c.Get("userRole")
if userRole != "admin" {
c.JSON(401, gin.H{
"message": "You are not authorized to perform this action",
"data": nil,
})
return
}
err := c.ShouldBindJSON(&input)
if err != nil {
c.JSON(400, gin.H{
"message": "Invalid input",
"data": err.Error(),
})
return
}

project, err := ph.ProjectService.CreateProject(&input)
if err != nil {
c.JSON(500, gin.H{
"message": "failed to create project",
"data": err.Error(),
})
return
}
c.JSON(200, gin.H{
"message": "successfully created project",
"data": project,
})
}

func (ph *ProjectHandler) GetAllProjectHandler(c *gin.Context) {
userRole := c.GetString("userRole")
projects, err := ph.ProjectService.GetProjects(userRole)
if err != nil {
c.JSON(500, gin.H{
"message": "failed to get projects",
"data": err.Error(),
})
return
}
c.JSON(200, gin.H{
"message": "got all projects",
"data": projects,
})
}
16 changes: 8 additions & 8 deletions Internal/api/handlers/userHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ func (uh *UserHandler) AllUserHandler(c *gin.Context) {

func (uh *UserHandler) UpdateRoleHandler(c *gin.Context) {
var input services.RoleInput
//userRole, _ := c.Get("userRole")
//if userRole != "admin" {
// c.JSON(400, gin.H{
// "message": "Not allowed to update role",
// "data": false,
// })
// return
//}
userRole, _ := c.Get("userRole")
if userRole != "admin" {
c.JSON(400, gin.H{
"message": "Not allowed to update role",
"data": false,
})
return
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{
"message": "input not valid",
Expand Down
3 changes: 2 additions & 1 deletion Internal/api/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import (
func AuthMiddleware(userRepo *repository.UserRepository, cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
if cfg.AuthMode == "false" {
c.Set("userID", 1)
c.Set("userID", uint(1))
c.Set("userRole", "admin")
c.Next()
return
}
Expand Down
11 changes: 11 additions & 0 deletions Internal/core/domain/projectdb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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"`
CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
}
47 changes: 47 additions & 0 deletions Internal/core/services/projectService.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package services

import (
"errors"

"github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository"
"github.com/Arjuna-Ragil/Localbase/Internal/core/domain"
)

type ProjectService struct {
ProjectRepo *repository.ProjectRepo
}

func NewProjectService(projectRepo *repository.ProjectRepo) *ProjectService {
return &ProjectService{ProjectRepo: projectRepo}
}

type CreateInput struct {
Name string `json:"name"`
Desc string `json:"desc"`
AdminID uint `json:"admin_id"`
}

func (ps *ProjectService) CreateProject(input *CreateInput) (*domain.Project, error) {
projectInfo := domain.Project{
Name: input.Name,
Desc: input.Desc,
AdminID: input.AdminID,
}
project, err := ps.ProjectRepo.CreateProject(&projectInfo)
if err != nil {
return nil, err
}
return project, nil
}

func (ps *ProjectService) GetProjects(Role string) ([]domain.Project, error) {
if Role != "admin" {
return nil, errors.New("permission denied")
}
var projects []domain.Project
project, err := ps.ProjectRepo.FetchProjects(projects)
if err != nil {
return nil, err
}
return project, nil
}
24 changes: 24 additions & 0 deletions Lb-web/src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react"

import { cn } from "@/lib/utils"

export interface TextareaProps

Check failure on line 5 in Lb-web/src/components/ui/textarea.tsx

View workflow job for this annotation

GitHub Actions / Frontend integrity checks

An interface declaring no members is equivalent to its supertype
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"

export { Textarea }
14 changes: 13 additions & 1 deletion Lb-web/src/features/Login/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import api from "@/services/api";

interface User {
id: number;
username: string;
email: string;
role: string;
}

interface AuthContextType {
isAuthenticated: boolean | null;
user: User | null;
role: string | null;
loading: boolean;
login: () => void; // call this after successful login to update state
Expand All @@ -13,16 +21,19 @@

export function AuthProvider({ children }: { children: ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const [user, setUser] = useState<User | null>(null);
const [role, setRole] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

const checkAuth = async () => {
try {
const response = await api.get('/protected/user/me');
setIsAuthenticated(true);
setUser(response.data.data);
setRole(response.data.data.role);
} catch {
setIsAuthenticated(false);
setUser(null);
setRole(null);
} finally {
setLoading(false);
Expand All @@ -45,18 +56,19 @@
console.error("Logout failed", error);
} finally {
setIsAuthenticated(false);
setUser(null);
setRole(null);
}
};

return (
<AuthContext.Provider value={{ isAuthenticated, role, loading, login, logout }}>
<AuthContext.Provider value={{ isAuthenticated, user, role, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}

export function useAuth() {

Check failure on line 71 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
109 changes: 109 additions & 0 deletions Lb-web/src/features/project/components/CreateProjectModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { useAuth } from "@/features/Login/context/AuthContext";
import { projectService } from "@/features/project/services/projectService";
import { Loader2 } from "lucide-react";

interface CreateProjectModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}

export function CreateProjectModal({ isOpen, onClose, onSuccess }: CreateProjectModalProps) {
const { user } = useAuth();
const [name, setName] = useState("");
const [desc, setDesc] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);

if (!user) {
setError("User not found");
return;
}

setIsLoading(true);

try {
await projectService.createProject({
name,
desc,
admin_id: user.id
});
onSuccess();
onClose();
// Reset form
setName("");
setDesc("");
} catch (err: any) {

Check failure on line 46 in Lb-web/src/features/project/components/CreateProjectModal.tsx

View workflow job for this annotation

GitHub Actions / Frontend integrity checks

Unexpected any. Specify a different type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setError((err as any).response?.data?.message || "Failed to create project");
} finally {
setIsLoading(false);
}
};

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
<DialogDescription>
Enter the details for your new project. Click create when you're done.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input
id="name"
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)}
className="col-span-3"
placeholder="My Awesome Project"
required
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="desc" className="text-right">
Description
</Label>
<Textarea
id="desc"
value={desc}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDesc(e.target.value)}
className="col-span-3"
placeholder="A brief description..."
/>
</div>
{error && (
<div className="text-red-500 text-sm text-center">
{error}
</div>
)}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose} disabled={isLoading}>
Cancel
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Project
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
Loading
Loading