From d232985cb8f90e4bd8a9d4138ad9afa8bd4b3479 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Mon, 12 Jan 2026 10:46:23 +0700 Subject: [PATCH 01/18] hotfix: dev Cors --- Backend/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Backend/main.go b/Backend/main.go index f7ac912..632515b 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -32,8 +32,8 @@ func main() { r := gin.Default() r.Use(cors.New(cors.Config{ - AllowOrigins: []string{"http://localhost:5173", "http://100.111.195.90:3001", "https://core-life.arjunaa.my.id"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowOrigins: []string{"http://localhost:5173", "http://100.111.195.90:3001", "https://core-life-dev.arjunaa.my.id", "https://core-life.arjunaa.my.id"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, AllowCredentials: true, MaxAge: 12 * time.Hour, From 01244ed5b1c9495c4360d61948f6613759d77307 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Mon, 12 Jan 2026 11:16:17 +0700 Subject: [PATCH 02/18] feat: main dashboard updated --- Backend/controller/auth/modelDB.go | 9 +- .../academic/hooks/useAcademicDashboard.ts | 104 ++++++++++++++++++ Frontend/project-CL/src/pages/home.tsx | 70 +++++++----- Frontend/project-CL/src/pages/profile.tsx | 0 4 files changed, 151 insertions(+), 32 deletions(-) create mode 100644 Frontend/project-CL/src/features/academic/hooks/useAcademicDashboard.ts create mode 100644 Frontend/project-CL/src/pages/profile.tsx diff --git a/Backend/controller/auth/modelDB.go b/Backend/controller/auth/modelDB.go index dd18fcb..4c5ace7 100644 --- a/Backend/controller/auth/modelDB.go +++ b/Backend/controller/auth/modelDB.go @@ -1,8 +1,9 @@ package auth type User struct { - ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` - Username string `gorm:"size:255;not null" json:"username"` - Email string `gorm:"size:255;not null" json:"email"` - Password string `json:"-"` + ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` + Username string `gorm:"size:255;not null" json:"username"` + Email string `gorm:"size:255;not null" json:"email"` + Password string `json:"-"` + TeleUsername string `gorm:"size:255" json:"tele_username"` } diff --git a/Frontend/project-CL/src/features/academic/hooks/useAcademicDashboard.ts b/Frontend/project-CL/src/features/academic/hooks/useAcademicDashboard.ts new file mode 100644 index 0000000..5f72b14 --- /dev/null +++ b/Frontend/project-CL/src/features/academic/hooks/useAcademicDashboard.ts @@ -0,0 +1,104 @@ +import { useState, useEffect } from 'react'; +import { academicService, type Subject, type Task } from '../services/academicService'; + +export interface AcademicMetrics { + gpa: number; + totalSks: number; + pendingTasks: number; + upcomingDeadlines: Task[]; +} + +export const useAcademicDashboard = () => { + const [subjects, setSubjects] = useState([]); + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [metrics, setMetrics] = useState({ + gpa: 0, + totalSks: 0, + pendingTasks: 0, + upcomingDeadlines: [] + }); + + const refreshData = async () => { + setLoading(true); + try { + const [subjectData, taskData] = await Promise.all([ + academicService.getSubjects(), + academicService.getTasks() + ]); + + setSubjects(subjectData); + setTasks(taskData); + + calculateMetrics(subjectData, taskData); + } catch (err) { + console.error("Failed to fetch academic data", err); + setError("Failed to load academic data"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + refreshData(); + }, []); + + const calculateMetrics = (subs: Subject[], tasks: Task[]) => { + // Calculate GPA + let totalPoints = 0; + let totalSks = 0; + + subs.forEach(sub => { + const sks = parseInt(sub.sks) || 0; + const points = getGradePoints(sub.grade); + + // Only count if grade is valid (not empty/in-progress if applicable) + // Assuming empty grade means in progress, maybe don't count? + // For now, if we have a grade, we count it. + if (sub.grade && sks > 0) { + totalPoints += points * sks; + totalSks += sks; + } + }); + + const gpa = totalSks > 0 ? totalPoints / totalSks : 0; + + // Calculate Pending Tasks + const pending = tasks.filter(t => t.status !== 'Completed' && t.status !== 'Done'); + + // Get upcoming deadlines (next 7 days) + const now = new Date(); + const nextWeek = new Date(); + nextWeek.setDate(now.getDate() + 7); + + const upcoming = pending.filter(t => { + const deadline = new Date(t.deadline); + return deadline >= now && deadline <= nextWeek; + }).sort((a, b) => new Date(a.deadline).getTime() - new Date(b.deadline).getTime()); + + setMetrics({ + gpa, + totalSks, + pendingTasks: pending.length, + upcomingDeadlines: upcoming + }); + }; + + const getGradePoints = (grade: string): number => { + switch (grade.toUpperCase()) { + case 'A': return 4.0; + case 'A-': return 3.7; + case 'B+': return 3.3; + case 'B': return 3.0; + case 'B-': return 2.7; + case 'C+': return 2.3; + case 'C': return 2.0; + case 'D': return 1.0; + default: return 0.0; + } + }; + + return { metrics, loading, error, subjects, tasks, refreshData }; +}; diff --git a/Frontend/project-CL/src/pages/home.tsx b/Frontend/project-CL/src/pages/home.tsx index b2055a5..11d7b3a 100644 --- a/Frontend/project-CL/src/pages/home.tsx +++ b/Frontend/project-CL/src/pages/home.tsx @@ -1,8 +1,22 @@ import Header from "../components/Header"; import ModuleCard from "../components/ModuleCard"; import Card from "../components/Card"; +import { useFinancialDashboard } from "../features/financial/hooks/useFinancialDashboard"; +import { useAcademicDashboard } from "../features/academic/hooks/useAcademicDashboard"; export default function Home() { + const { metrics: finMetrics, loading: finLoading } = useFinancialDashboard(); + const { metrics: acadMetrics, loading: acadLoading } = useAcademicDashboard(); + + const formatCurrency = (val: number) => { + if (Math.abs(val) >= 1_000_000_000) { + return `Rp ${(val / 1_000_000_000).toFixed(1)}B`; + } else if (Math.abs(val) >= 1_000_000) { + return `Rp ${(val / 1_000_000).toFixed(1)}M`; + } + return `Rp ${(val / 1000).toFixed(0)}k`; + }; + return (
@@ -12,18 +26,30 @@ export default function Home() {
Total Assets
-
RP 12.5M
-
+2.5% this month
+
+ {finLoading ? "Loading..." : formatCurrency(finMetrics.balance)} +
+
+ {finLoading ? "..." : (finMetrics.totalIncome > 0 ? "Active" : "No Income")} +
Pending Tasks
-
5
-
3 High Priority
+
+ {acadLoading ? "..." : acadMetrics.pendingTasks} +
+
+ {acadLoading ? "..." : `${acadMetrics.upcomingDeadlines.length} Upcoming`} +
Academic Status
-
3.8 GPA
-
Excellent
+
+ {acadLoading ? "..." : `${acadMetrics.gpa.toFixed(2)} GPA`} +
+
+ {acadLoading ? "..." : `${acadMetrics.totalSks} SKS Completed`} +
@@ -56,31 +82,19 @@ export default function Home() { } color="bg-emerald-50 text-emerald-600" /> - - - - } - color="bg-amber-50 text-amber-600" - /> - - + + {/* More Modules Placeholder */} +
+
+ + - } - color="bg-rose-50 text-rose-600" - /> +
+ More modules to come +
) -} \ No newline at end of file +} diff --git a/Frontend/project-CL/src/pages/profile.tsx b/Frontend/project-CL/src/pages/profile.tsx new file mode 100644 index 0000000..e69de29 From de3ba87cb807c9564900b6760cf90ac845b09bd0 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Mon, 12 Jan 2026 16:44:23 +0700 Subject: [PATCH 03/18] feat: Profile page, login with email/username, More input in register --- Backend/controller/auth/handler.go | 39 ++- Backend/controller/auth/middleware.go | 3 +- Backend/internal/profile/handler.go | 35 +++ .../auth => internal/profile}/modelDB.go | 6 +- Backend/internal/profile/router.go | 11 + Backend/main.go | 4 +- Frontend/project-CL/src/components/Button.tsx | 39 +++ Frontend/project-CL/src/components/Input.tsx | 46 ++++ .../project-CL/src/components/Sidebar.tsx | 20 +- .../features/auth/components/LoginForm.tsx | 2 +- .../features/auth/components/RegisterForm.tsx | 48 +++- .../src/features/auth/hooks/useLogin.ts | 9 +- .../profile/services/profileService.ts | 23 ++ Frontend/project-CL/src/main.tsx | 5 + Frontend/project-CL/src/pages/profile.tsx | 260 ++++++++++++++++++ 15 files changed, 530 insertions(+), 20 deletions(-) create mode 100644 Backend/internal/profile/handler.go rename Backend/{controller/auth => internal/profile}/modelDB.go (55%) create mode 100644 Backend/internal/profile/router.go create mode 100644 Frontend/project-CL/src/components/Button.tsx create mode 100644 Frontend/project-CL/src/components/Input.tsx create mode 100644 Frontend/project-CL/src/features/profile/services/profileService.ts diff --git a/Backend/controller/auth/handler.go b/Backend/controller/auth/handler.go index 17a81e1..2c8ae4a 100644 --- a/Backend/controller/auth/handler.go +++ b/Backend/controller/auth/handler.go @@ -4,6 +4,7 @@ import ( "net/http" "os" "personal-erp-backend/database" + "personal-erp-backend/internal/profile" "time" "github.com/gin-gonic/gin" @@ -15,9 +16,14 @@ var SecretKey = []byte(os.Getenv("SECRET_KEY")) func Register(c *gin.Context) { var input struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + PhoneNumber string `json:"phone_number"` + Country string `json:"country"` + TeleUsername string `json:"tele_username"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -28,25 +34,40 @@ func Register(c *gin.Context) { c.JSON(500, gin.H{"error": err.Error()}) return } - user := User{Username: input.Username, Email: input.Email, Password: string(hashedPassword)} + user := profile.User{ + Username: input.Username, + Email: input.Email, + Password: string(hashedPassword), + FirstName: input.FirstName, + LastName: input.LastName, + PhoneNumber: input.PhoneNumber, + Country: input.Country, + TeleUsername: input.TeleUsername, + } if err := database.DB.Create(&user).Error; err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"user": user}) + + if err := Login; err != nil { + c.JSON(500, gin.H{"error": err}) + return + } } func Login(c *gin.Context) { var input struct { Username string `json:"username"` + Email string `json:"email"` Password string `json:"password"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - var user User - if err := database.DB.Where("username = ?", input.Username).First(&user).Error; err != nil { + var user profile.User + if err := database.DB.Where("username = ? OR email = ?", input.Username, input.Email).First(&user).Error; err != nil { c.JSON(404, gin.H{"error": err.Error()}) return } @@ -66,6 +87,12 @@ func Login(c *gin.Context) { c.SetSameSite(http.SameSiteLaxMode) c.SetCookie("authorization", tokenString, 3600*24, "", "", false, true) c.JSON(http.StatusOK, gin.H{"token": tokenString, "message": "success"}) + + if err := profile.GetUser; err != nil { + c.JSON(404, gin.H{"message": err}) + return + } + c.JSON(200, gin.H{"message": "OK", "user": user}) } func Logout(c *gin.Context) { diff --git a/Backend/controller/auth/middleware.go b/Backend/controller/auth/middleware.go index faa200a..c1c73f4 100644 --- a/Backend/controller/auth/middleware.go +++ b/Backend/controller/auth/middleware.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "personal-erp-backend/database" + "personal-erp-backend/internal/profile" "time" "github.com/gin-gonic/gin" @@ -42,7 +43,7 @@ func RequireAuth(c *gin.Context) { if float64(time.Now().Unix()) > claims["exp"].(float64) { c.JSON(401, gin.H{"message": "Token expired"}) } - var user User + var user profile.User if result := database.DB.First(&user, claims["sub"]); result.Error != nil { c.AbortWithStatusJSON(401, gin.H{"error": "User no"}) return diff --git a/Backend/internal/profile/handler.go b/Backend/internal/profile/handler.go new file mode 100644 index 0000000..48b727e --- /dev/null +++ b/Backend/internal/profile/handler.go @@ -0,0 +1,35 @@ +package profile + +import ( + "personal-erp-backend/database" + + "github.com/gin-gonic/gin" +) + +func GetUser(c *gin.Context) { + var user User + userID, _ := c.Get("userID") + if err := database.DB.Where("id = ?", userID).Find(&user).Error; err != nil { + c.JSON(404, gin.H{"message": err}) + return + } + c.JSON(200, gin.H{"Success": user}) +} + +func UpdateUser(c *gin.Context) { + var user User + userID, _ := c.Get("userID") + if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil { + c.JSON(404, gin.H{"message": err}) + return + } + if err := c.ShouldBind(&user); err != nil { + c.JSON(400, gin.H{"message": err}) + return + } + if err := database.DB.Model(&user).Updates(&user).Error; err != nil { + c.JSON(404, gin.H{"message": err}) + return + } + c.JSON(200, gin.H{"Success": user}) +} diff --git a/Backend/controller/auth/modelDB.go b/Backend/internal/profile/modelDB.go similarity index 55% rename from Backend/controller/auth/modelDB.go rename to Backend/internal/profile/modelDB.go index 4c5ace7..bf8b0ce 100644 --- a/Backend/controller/auth/modelDB.go +++ b/Backend/internal/profile/modelDB.go @@ -1,9 +1,13 @@ -package auth +package profile type User struct { ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` Username string `gorm:"size:255;not null" json:"username"` Email string `gorm:"size:255;not null" json:"email"` Password string `json:"-"` + FirstName string `gorm:"size:255" json:"first_name"` + LastName string `gorm:"size:255" json:"last_name"` + PhoneNumber string `gorm:"size:255" json:"phone_number"` + Country string `gorm:"size:255" json:"country"` TeleUsername string `gorm:"size:255" json:"tele_username"` } diff --git a/Backend/internal/profile/router.go b/Backend/internal/profile/router.go new file mode 100644 index 0000000..3e7ae6b --- /dev/null +++ b/Backend/internal/profile/router.go @@ -0,0 +1,11 @@ +package profile + +import "github.com/gin-gonic/gin" + +func RegisterRouter(r *gin.RouterGroup) { + profile := r.Group("/profile") + { + profile.GET("/user", GetUser) + profile.POST("/user", UpdateUser) + } +} diff --git a/Backend/main.go b/Backend/main.go index 632515b..87019e8 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -6,6 +6,7 @@ import ( "personal-erp-backend/database" "personal-erp-backend/internal/academic" "personal-erp-backend/internal/finance" + "personal-erp-backend/internal/profile" "time" "github.com/gin-contrib/cors" @@ -16,7 +17,7 @@ func main() { database.ConnectDB() err := database.DB.AutoMigrate( - &auth2.User{}, + &profile.User{}, &academic.Subject{}, &academic.Task{}, &academic.Note{}, @@ -49,6 +50,7 @@ func main() { protected := r.Group("/api") protected.Use(auth2.RequireAuth) { + profile.RegisterRouter(protected) academic.RegisterRoutes(protected) finance.RegisterRouter(protected) } diff --git a/Frontend/project-CL/src/components/Button.tsx b/Frontend/project-CL/src/components/Button.tsx new file mode 100644 index 0000000..21e05be --- /dev/null +++ b/Frontend/project-CL/src/components/Button.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Loader2 } from 'lucide-react'; + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; + isLoading?: boolean; + icon?: React.ReactNode; +} + +export default function Button({ + children, + variant = 'primary', + isLoading = false, + icon, + className = '', + disabled, + ...props +}: ButtonProps) { + const baseStyles = "inline-flex items-center justify-center -gap-1 rounded-xl text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed active:scale-[0.98]"; + + const variants = { + primary: "bg-indigo-600 text-white hover:bg-indigo-700 hover:shadow-lg hover:shadow-indigo-200 focus:ring-indigo-500 border border-transparent", + secondary: "bg-white text-slate-700 border border-slate-200 hover:bg-slate-50 hover:border-slate-300 focus:ring-slate-200 shadow-sm", + danger: "bg-red-50 text-red-600 hover:bg-red-100 border border-transparent focus:ring-red-500", + ghost: "bg-transparent text-slate-600 hover:bg-slate-100 hover:text-slate-900 border border-transparent", + }; + + return ( + + ); +} diff --git a/Frontend/project-CL/src/components/Input.tsx b/Frontend/project-CL/src/components/Input.tsx new file mode 100644 index 0000000..251f1f4 --- /dev/null +++ b/Frontend/project-CL/src/components/Input.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +interface InputProps extends React.InputHTMLAttributes { + label?: string; + error?: string; + icon?: React.ReactNode; +} + +export default function Input({ label, error, icon, className = '', ...props }: InputProps) { + return ( +
+ {label && ( + + )} +
+ {icon && ( +
+ {icon} +
+ )} + +
+ {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/Frontend/project-CL/src/components/Sidebar.tsx b/Frontend/project-CL/src/components/Sidebar.tsx index 14a653c..d924b5f 100644 --- a/Frontend/project-CL/src/components/Sidebar.tsx +++ b/Frontend/project-CL/src/components/Sidebar.tsx @@ -1,8 +1,7 @@ import { NavLink } from "react-router"; -import { useAuth } from "../features/auth/context/AuthContext"; + export default function Sidebar() { - const { logout } = useAuth(); const links = [ { name: "Home", path: "/", icon: "🏠" }, { name: "Academic", path: "/academic", icon: "🎓" }, @@ -36,13 +35,18 @@ export default function Sidebar() { ))}
- + 👤 + Profile +
diff --git a/Frontend/project-CL/src/features/auth/components/LoginForm.tsx b/Frontend/project-CL/src/features/auth/components/LoginForm.tsx index 947ca6b..5023080 100644 --- a/Frontend/project-CL/src/features/auth/components/LoginForm.tsx +++ b/Frontend/project-CL/src/features/auth/components/LoginForm.tsx @@ -13,7 +13,7 @@ export function LoginForm({ onRegisterClick }: LoginFormProps) { return (
+
+ + +
+ + + +
+ + +
+ + + { setError(null); const formData = new FormData(e.currentTarget); - const data = Object.fromEntries(formData); + const rawData = Object.fromEntries(formData); + + // Backend expects username OR email. We send the same value for both + // so the backend query (username = ? || email = ?) works for either. + const data = { + ...rawData, + email: rawData.username // Duplicate username to email + }; try { const response = await authService.login(data); diff --git a/Frontend/project-CL/src/features/profile/services/profileService.ts b/Frontend/project-CL/src/features/profile/services/profileService.ts new file mode 100644 index 0000000..1851dc2 --- /dev/null +++ b/Frontend/project-CL/src/features/profile/services/profileService.ts @@ -0,0 +1,23 @@ +import api from '../../../services/api'; + +export interface UserProfile { + id: number; + username: string; + email: string; + first_name: string; + last_name: string; + phone_number: string; + country: string; + tele_username: string; +} + +export const profileService = { + getUser: async () => { + const response = await api.get<{ Success: UserProfile }>('/profile/user'); + return response.data.Success; + }, + updateUser: async (data: Partial) => { + const response = await api.post<{ Success: UserProfile }>('/profile/user', data); + return response.data.Success; + } +}; diff --git a/Frontend/project-CL/src/main.tsx b/Frontend/project-CL/src/main.tsx index ea21ba4..4f15ccd 100644 --- a/Frontend/project-CL/src/main.tsx +++ b/Frontend/project-CL/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import './index.css' import { RouterProvider } from 'react-router' import Home from './pages/home' +import Profile from './pages/profile' import { createBrowserRouter } from 'react-router' import Academic from './features/academic/academic' import SidebarLayout from './layouts/SidebarLayout' @@ -33,6 +34,10 @@ const router = createBrowserRouter([ path: '/finance', element: , }, + { + path: '/profile', + element: , + }, ] } ] diff --git a/Frontend/project-CL/src/pages/profile.tsx b/Frontend/project-CL/src/pages/profile.tsx index e69de29..a349c0b 100644 --- a/Frontend/project-CL/src/pages/profile.tsx +++ b/Frontend/project-CL/src/pages/profile.tsx @@ -0,0 +1,260 @@ +import { useState, useEffect } from 'react'; +import { User, Mail, Shield, Camera, Save, Phone, MapPin, Send } from 'lucide-react'; +import Card from '../components/Card'; +import Input from '../components/Input'; +import Button from '../components/Button'; +import { useAuth } from '../features/auth/context/AuthContext'; +import { LogOut } from 'lucide-react'; +import { profileService } from '../features/profile/services/profileService'; + +export default function Profile() { + const { logout } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [formData, setFormData] = useState({ + username: '', + email: '', + first_name: '', + last_name: '', + phone_number: '', + country: '', + tele_username: '', + currentPassword: '', + newPassword: '', + confirmPassword: '', + }); + + useEffect(() => { + const fetchUser = async () => { + try { + const user = await profileService.getUser(); + setFormData(prev => ({ + ...prev, + username: user.username, + email: user.email, + first_name: user.first_name || '', + last_name: user.last_name || '', + phone_number: user.phone_number || '', + country: user.country || '', + tele_username: user.tele_username || '', + })); + } catch (error) { + console.error("Failed to fetch user:", error); + } + }; + fetchUser(); + }, []); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + try { + await profileService.updateUser({ + first_name: formData.first_name, + last_name: formData.last_name, + phone_number: formData.phone_number, + country: formData.country, + tele_username: formData.tele_username, + }); + alert("Profile updated successfully"); + } catch (error) { + console.error("Failed to update profile:", error); + alert("Failed to update profile"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* Header */} +
+

Profile Settings

+

Manage your account settings and preferences.

+
+ +
+ {/* Left Column - Profile Card */} +
+ +
+
+ + {formData.username.charAt(0).toUpperCase()} + +
+
+ +
+
+

{formData.username}

+

Administrator

+
+ +
+
+
+ +
+ Personal Information +
+
+
+ +
+ Security & Privacy +
+ + +
+
+ + {/* Right Column - Edit Form */} +
+ + +
+
+

+ + Basic Information +

+
+ +
+ + +
+ +
+ } + /> + } + /> +
+ +
+ +
+ } + readOnly + className="bg-slate-50" + /> + } + /> +
+
+
+ +
+
+

+ + Security +

+
+ +
+ +
+ + +
+
+
+ +
+ + +
+ +
+
+
+
+ ); +} From 9aa3b9b25f560bc9d72500a2bd19709bb4ee70b5 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Tue, 13 Jan 2026 17:10:25 +0700 Subject: [PATCH 04/18] feat: seaweedfs (bucket) docker --- docker-compose.dev.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index b1afa5a..93eca5e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -39,9 +39,22 @@ services: networks: - erp_network_dev + seaweedfs: + image: chrislusf/seaweedfs + container_name: cl_bucket_dev + restart: always + ports: + - "8333:8333" + - "8888:8888" + command: "server -s3 -filer -s3.port=8333 -filer.port=8888 -master.volumeSizeLimitMB=1024" + volumes: + - seaweed_data_dev:/data + networks: + - erp_network_dev + volumes: pg_data_dev: - + seaweed_data_dev: networks: erp_network_dev: driver: bridge \ No newline at end of file From 687336a011efcb7a2adb41e28af4fe17aef5c573 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Wed, 14 Jan 2026 18:50:00 +0700 Subject: [PATCH 05/18] feat: Storage bucket connected --- Backend/database/bucket.go | 25 +++++++++++++++++++++++++ Backend/go.mod | 12 ++++++++++++ Backend/go.sum | 23 +++++++++++++++++++++++ Backend/main.go | 1 + 4 files changed, 61 insertions(+) create mode 100644 Backend/database/bucket.go diff --git a/Backend/database/bucket.go b/Backend/database/bucket.go new file mode 100644 index 0000000..796afb4 --- /dev/null +++ b/Backend/database/bucket.go @@ -0,0 +1,25 @@ +package database + +import ( + "log" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +func InitBucket() *minio.Client { + endpoint := "127.0.0.1:8333" + accessKeyID := "" + secretAccessKey := "" + useSSL := false + + minioClient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), + Secure: useSSL, + }) + if err != nil { + log.Fatalln("Failed to initialize minio client:", err) + } + log.Println("Successfully connected to Minio") + return minioClient +} diff --git a/Backend/go.mod b/Backend/go.mod index a91da1c..e0dba23 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -15,14 +15,17 @@ require ( github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.6 // indirect @@ -30,14 +33,22 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/crc64nvme v1.1.0 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.97 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.58.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.uber.org/mock v0.6.0 // indirect @@ -48,4 +59,5 @@ require ( golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/Backend/go.sum b/Backend/go.sum index 83424c0..cee7637 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -9,6 +9,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= @@ -17,6 +19,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -34,6 +38,8 @@ github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -50,12 +56,23 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= +github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ= +github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -63,12 +80,16 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -81,6 +102,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= diff --git a/Backend/main.go b/Backend/main.go index 87019e8..76e4de8 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -15,6 +15,7 @@ import ( func main() { database.ConnectDB() + database.InitBucket() err := database.DB.AutoMigrate( &profile.User{}, From bae80b96c8ff87476119b2562cc63d88487268d1 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Thu, 15 Jan 2026 13:39:32 +0700 Subject: [PATCH 06/18] feat: Storage bucket added --- Backend/database/bucket.go | 45 +++++++++++++++ Backend/internal/profile/handler.go | 33 +++++++++++ Backend/internal/profile/modelDB.go | 1 + Backend/internal/profile/router.go | 12 +++- .../profile/services/profileService.ts | 13 ++++- Frontend/project-CL/src/pages/profile.tsx | 57 +++++++++++++++++-- 6 files changed, 152 insertions(+), 9 deletions(-) diff --git a/Backend/database/bucket.go b/Backend/database/bucket.go index 796afb4..e389b31 100644 --- a/Backend/database/bucket.go +++ b/Backend/database/bucket.go @@ -1,7 +1,11 @@ package database import ( + "context" + "fmt" "log" + "mime/multipart" + "time" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" @@ -23,3 +27,44 @@ func InitBucket() *minio.Client { log.Println("Successfully connected to Minio") return minioClient } + +func UploadFile(client *minio.Client, file *multipart.FileHeader) (string, error) { + ctx := context.Background() + bucketName := "user-profile" + + src, err := file.Open() + if err != nil { + return "", err + } + defer func(src multipart.File) { + err := src.Close() + if err != nil { + + } + }(src) + + exists, err := client.BucketExists(ctx, bucketName) + if err != nil { + return "", err + } + if !exists { + err := client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) + if err != nil { + return "", fmt.Errorf("failed to create bucket %s: %v", bucketName, err) + } + log.Println("Successfully created bucket:", bucketName) + } + + objectName := fmt.Sprintf("file_%d_%s", time.Now().Unix(), file.Filename) + contentType := file.Header.Get("Content-Type") + + info, err := client.PutObject(ctx, bucketName, objectName, src, file.Size, minio.PutObjectOptions{ + ContentType: contentType, + }) + if err != nil { + return "", err + } + log.Println("Successfully uploaded file:", info) + fileURL := fmt.Sprintf("http://localhost:8333/%s/%s", bucketName, objectName) + return fileURL, nil +} diff --git a/Backend/internal/profile/handler.go b/Backend/internal/profile/handler.go index 48b727e..2298dbe 100644 --- a/Backend/internal/profile/handler.go +++ b/Backend/internal/profile/handler.go @@ -4,8 +4,17 @@ import ( "personal-erp-backend/database" "github.com/gin-gonic/gin" + "github.com/minio/minio-go/v7" ) +type UploadHandler struct { + minioClient *minio.Client +} + +func NewUploadHandler(client *minio.Client) *UploadHandler { + return &UploadHandler{minioClient: client} +} + func GetUser(c *gin.Context) { var user User userID, _ := c.Get("userID") @@ -33,3 +42,27 @@ func UpdateUser(c *gin.Context) { } c.JSON(200, gin.H{"Success": user}) } + +func (h *UploadHandler) UploadAvatar(c *gin.Context) { + file, err := c.FormFile("avatar") + if err != nil { + c.JSON(400, gin.H{"message": err}) + return + } + fileURL, err := database.UploadFile(h.minioClient, file) + if err != nil { + c.JSON(400, gin.H{"message": err}) + return + } + var user User + userID, _ := c.Get("userID") + if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil { + c.JSON(404, gin.H{"message": err}) + return + } + if err := database.DB.Model(&user).Update("profile_url", fileURL).Error; err != nil { + c.JSON(404, gin.H{"message": err}) + return + } + c.JSON(200, gin.H{"Success": fileURL}) +} diff --git a/Backend/internal/profile/modelDB.go b/Backend/internal/profile/modelDB.go index bf8b0ce..8aef97f 100644 --- a/Backend/internal/profile/modelDB.go +++ b/Backend/internal/profile/modelDB.go @@ -10,4 +10,5 @@ type User struct { PhoneNumber string `gorm:"size:255" json:"phone_number"` Country string `gorm:"size:255" json:"country"` TeleUsername string `gorm:"size:255" json:"tele_username"` + ProfileURL string `json:"profile_url"` } diff --git a/Backend/internal/profile/router.go b/Backend/internal/profile/router.go index 3e7ae6b..76478b4 100644 --- a/Backend/internal/profile/router.go +++ b/Backend/internal/profile/router.go @@ -1,11 +1,19 @@ package profile -import "github.com/gin-gonic/gin" +import ( + "personal-erp-backend/database" + + "github.com/gin-gonic/gin" +) func RegisterRouter(r *gin.RouterGroup) { + minioClient := database.InitBucket() + uploadHandler := NewUploadHandler(minioClient) + profile := r.Group("/profile") { profile.GET("/user", GetUser) - profile.POST("/user", UpdateUser) + profile.PUT("/user", UpdateUser) + profile.POST("/upload/avatar", uploadHandler.UploadAvatar) } } diff --git a/Frontend/project-CL/src/features/profile/services/profileService.ts b/Frontend/project-CL/src/features/profile/services/profileService.ts index 1851dc2..a2ffcba 100644 --- a/Frontend/project-CL/src/features/profile/services/profileService.ts +++ b/Frontend/project-CL/src/features/profile/services/profileService.ts @@ -9,6 +9,7 @@ export interface UserProfile { phone_number: string; country: string; tele_username: string; + profile_url?: string; } export const profileService = { @@ -17,7 +18,17 @@ export const profileService = { return response.data.Success; }, updateUser: async (data: Partial) => { - const response = await api.post<{ Success: UserProfile }>('/profile/user', data); + const response = await api.put<{ Success: UserProfile }>('/profile/user', data); + return response.data.Success; + }, + uploadAvatar: async (file: File) => { + const formData = new FormData(); + formData.append('avatar', file); + const response = await api.post<{ Success: string }>('/profile/upload/avatar', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); return response.data.Success; } }; diff --git a/Frontend/project-CL/src/pages/profile.tsx b/Frontend/project-CL/src/pages/profile.tsx index a349c0b..ab66671 100644 --- a/Frontend/project-CL/src/pages/profile.tsx +++ b/Frontend/project-CL/src/pages/profile.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { User, Mail, Shield, Camera, Save, Phone, MapPin, Send } from 'lucide-react'; import Card from '../components/Card'; import Input from '../components/Input'; @@ -21,7 +21,9 @@ export default function Profile() { currentPassword: '', newPassword: '', confirmPassword: '', + profile_url: '', }); + const fileInputRef = useRef(null); useEffect(() => { const fetchUser = async () => { @@ -36,6 +38,7 @@ export default function Profile() { phone_number: user.phone_number || '', country: user.country || '', tele_username: user.tele_username || '', + profile_url: user.profile_url || '', })); } catch (error) { console.error("Failed to fetch user:", error); @@ -69,6 +72,30 @@ export default function Profile() { } }; + const handleImageClick = () => { + fileInputRef.current?.click(); + }; + + const handleImageChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Optional: specific max size check or type check + if (file.size > 5 * 1024 * 1024) { // 5MB limit example + alert("File size too large (max 5MB)"); + return; + } + + try { + // Optimistic UI update or loading state could go here + const url = await profileService.uploadAvatar(file); + setFormData(prev => ({ ...prev, profile_url: url })); + } catch (error) { + console.error("Failed to upload avatar:", error); + alert("Failed to upload avatar"); + } + }; + return (
{/* Header */} @@ -81,11 +108,29 @@ export default function Profile() { {/* Left Column - Profile Card */}
-
-
- - {formData.username.charAt(0).toUpperCase()} - +
+ +
+ {formData.profile_url ? ( + Profile + ) : ( + + {formData.username.charAt(0).toUpperCase()} + + )}
From 5d8794b4409ec3b83eab82d887e08aa15f65a6af Mon Sep 17 00:00:00 2001 From: Arjuna Date: Thu, 15 Jan 2026 20:04:38 +0700 Subject: [PATCH 07/18] feat: Bucket safety config added --- .github/workflows/deploy.yml | 10 ++++++++++ .gitignore | 3 ++- Backend/database/bucket.go | 13 +++++++++---- docker-compose.dev.yml | 7 ++++--- docker-compose.prod.yml | 14 ++++++++++++++ seaweedfs_config/s3_config.template.json | 14 ++++++++++++++ 6 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 seaweedfs_config/s3_config.template.json diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 67b9d38..b6aff13 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,6 +27,16 @@ jobs: echo "${{ secrets.VITE_ENV_FILE }}" > .env.production cat .env.production + - name: Create seaweedfs config + env: + BUCKET_ACCESS_KEY: ${{ secrets.BUCKET_ACCESS_KEY }} + BUCKET_SECRET_KEY: ${{ secrets.BUCKET_SECRET_KEY }} + run: | + echo "Generating seaweedfs config" + mkdir -p seaweedfs_config + envsubst < seaweedfs_config/s3_config.template.json > seaweedfs_config/s3_config.json + echo "Seaweedfs config generated" + - name: Restart docker run: | echo "Restarting docker" diff --git a/.gitignore b/.gitignore index 42bb985..a683cc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env* .idea -docker-compose.override.yml \ No newline at end of file +docker-compose.override.yml +seaweedfs_config/s3_config.json \ No newline at end of file diff --git a/Backend/database/bucket.go b/Backend/database/bucket.go index e389b31..3bf5f63 100644 --- a/Backend/database/bucket.go +++ b/Backend/database/bucket.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "mime/multipart" + "os" "time" "github.com/minio/minio-go/v7" @@ -12,9 +13,9 @@ import ( ) func InitBucket() *minio.Client { - endpoint := "127.0.0.1:8333" - accessKeyID := "" - secretAccessKey := "" + endpoint := os.Getenv("BUCKET_ENDPOINT") + accessKeyID := os.Getenv("BUCKET_ACCESS_KEY") + secretAccessKey := os.Getenv("BUCKET_SECRET_KEY") useSSL := false minioClient, err := minio.New(endpoint, &minio.Options{ @@ -31,6 +32,10 @@ func InitBucket() *minio.Client { func UploadFile(client *minio.Client, file *multipart.FileHeader) (string, error) { ctx := context.Background() bucketName := "user-profile" + publicDomain := os.Getenv("BUCKET_PUBLIC_DOMAIN") + if publicDomain == "" { + publicDomain = "http://localhost:8334" + } src, err := file.Open() if err != nil { @@ -65,6 +70,6 @@ func UploadFile(client *minio.Client, file *multipart.FileHeader) (string, error return "", err } log.Println("Successfully uploaded file:", info) - fileURL := fmt.Sprintf("http://localhost:8333/%s/%s", bucketName, objectName) + fileURL := fmt.Sprintf("%s/%s/%s", publicDomain, bucketName, objectName) return fileURL, nil } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 93eca5e..d074e6f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -44,11 +44,12 @@ services: container_name: cl_bucket_dev restart: always ports: - - "8333:8333" - - "8888:8888" - command: "server -s3 -filer -s3.port=8333 -filer.port=8888 -master.volumeSizeLimitMB=1024" + - "8334:8333" + - "8889:8888" + command: "server -s3 -filer -s3.port=8333 -filer.port=8888 -master.volumeSizeLimitMB=1024 -volume.max=100 -s3.config=/etc/seaweedfs/s3_config.json" volumes: - seaweed_data_dev:/data + - ./seaweedfs_config/s3_config.json:/etc/seaweedfs/s3_config.json networks: - erp_network_dev diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index fa23fb5..d7eaac4 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -41,8 +41,22 @@ services: networks: - erp_network_prod + seaweedfs: + image: chrislusf/seaweedfs + container_name: cl_bucket_production + restart: always + ports: + - "8333:8333" + - "8888:8888" + command: "server -s3 -filer -s3.port=8333 -filer.port=8888 -master.volumeSizeLimitMB=1024 volume.max=100" + volumes: + - seaweed_data_prod:/data + networks: + - erp_network_prod + volumes: pg_data_prod: + seaweed_data_prod: networks: erp_network_prod: diff --git a/seaweedfs_config/s3_config.template.json b/seaweedfs_config/s3_config.template.json new file mode 100644 index 0000000..7694a88 --- /dev/null +++ b/seaweedfs_config/s3_config.template.json @@ -0,0 +1,14 @@ +{ + "identities": [ + { + "name": "Core-Life-Bucket", + "credentials": [ + { + "accessKey": "$BUCKET_ACCESS_KEY", + "secretKey": "$BUCKET_SECRET_KEY" + } + ], + "actions": [ "Read", "Write", "List", "Tagging", "Admin"] + } + ] +} From 9df71183db628a4823ccb2f85251b5c55eb80855 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 06:26:30 +0700 Subject: [PATCH 08/18] hotfix: env restart --- docker-compose.dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index d074e6f..72a30ba 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -39,6 +39,7 @@ services: networks: - erp_network_dev + # --- BUCKET (SEAWEDDFS) --- seaweedfs: image: chrislusf/seaweedfs container_name: cl_bucket_dev From 4013fe9112e12a818e978ea5ca6f4555c80323b7 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 06:56:24 +0700 Subject: [PATCH 09/18] hotfix: test deploy --- docker-compose.dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 72a30ba..bd0985c 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -39,7 +39,7 @@ services: networks: - erp_network_dev - # --- BUCKET (SEAWEDDFS) --- + # --- BUCKETS (SEAWEDDFS) --- seaweedfs: image: chrislusf/seaweedfs container_name: cl_bucket_dev From 1fd9e4d3349c9e41317ce96f8d4348cc7a4298b2 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 08:39:52 +0700 Subject: [PATCH 10/18] hotfix: CI bucket config debugging --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b6aff13..f8d6c07 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -33,9 +33,11 @@ jobs: BUCKET_SECRET_KEY: ${{ secrets.BUCKET_SECRET_KEY }} run: | echo "Generating seaweedfs config" + ls -R mkdir -p seaweedfs_config envsubst < seaweedfs_config/s3_config.template.json > seaweedfs_config/s3_config.json echo "Seaweedfs config generated" + cat seaweedfs_config/s3_config.json - name: Restart docker run: | From 56131db08bcc98cce7adf58657824646eda37d85 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 08:57:47 +0700 Subject: [PATCH 11/18] feat: added manual rerun deploy workflow --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f8d6c07..a66e6d5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,6 +5,7 @@ on: branches: - main - dev + workflow_dispatch: jobs: deploy-dev: From 8631b4a85015f1e0a1001f6105ec10381a6be468 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 09:07:25 +0700 Subject: [PATCH 12/18] hotfix: wrong fe env fix --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a66e6d5..51036da 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,8 +25,8 @@ jobs: - name: Create fe env run: | cd Frontend/project-CL - echo "${{ secrets.VITE_ENV_FILE }}" > .env.production - cat .env.production + echo "${{ secrets.VITE_ENV_FILE }}" > .env.development + cat .env.development - name: Create seaweedfs config env: From 369983b99026d7697389dcaca00680d7b7ef74fa Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 09:25:26 +0700 Subject: [PATCH 13/18] hotfix: revert to previous, maybe it will work?? --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 51036da..a66e6d5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,8 +25,8 @@ jobs: - name: Create fe env run: | cd Frontend/project-CL - echo "${{ secrets.VITE_ENV_FILE }}" > .env.development - cat .env.development + echo "${{ secrets.VITE_ENV_FILE }}" > .env.production + cat .env.production - name: Create seaweedfs config env: From f342f3a365a89fcc19a6add34b0afd78ac089fcc Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 09:33:04 +0700 Subject: [PATCH 14/18] hotfix: maybe env dev will work? --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a66e6d5..51036da 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,8 +25,8 @@ jobs: - name: Create fe env run: | cd Frontend/project-CL - echo "${{ secrets.VITE_ENV_FILE }}" > .env.production - cat .env.production + echo "${{ secrets.VITE_ENV_FILE }}" > .env.development + cat .env.development - name: Create seaweedfs config env: From 57c0088632290faffd201f8a4b3d21b192adbaf1 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 09:39:46 +0700 Subject: [PATCH 15/18] hotfix: change back --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 51036da..a66e6d5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,8 +25,8 @@ jobs: - name: Create fe env run: | cd Frontend/project-CL - echo "${{ secrets.VITE_ENV_FILE }}" > .env.development - cat .env.development + echo "${{ secrets.VITE_ENV_FILE }}" > .env.production + cat .env.production - name: Create seaweedfs config env: From 8fc167afb84687ed41faee47fd71c9cafb9def42 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 10:19:34 +0700 Subject: [PATCH 16/18] feat: update storage bucket protection --- docker-compose.dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index bd0985c..9d6d275 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -46,7 +46,7 @@ services: restart: always ports: - "8334:8333" - - "8889:8888" + - "127.0.0.1:8889:8888" command: "server -s3 -filer -s3.port=8333 -filer.port=8888 -master.volumeSizeLimitMB=1024 -volume.max=100 -s3.config=/etc/seaweedfs/s3_config.json" volumes: - seaweed_data_dev:/data From f24ad18bb48f8bd03c7bdb6fa51195baaeb648d5 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 10:54:57 +0700 Subject: [PATCH 17/18] hotfix: make bucket config receive anonymous user --- seaweedfs_config/s3_config.template.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/seaweedfs_config/s3_config.template.json b/seaweedfs_config/s3_config.template.json index 7694a88..972a6fc 100644 --- a/seaweedfs_config/s3_config.template.json +++ b/seaweedfs_config/s3_config.template.json @@ -9,6 +9,10 @@ } ], "actions": [ "Read", "Write", "List", "Tagging", "Admin"] + }, + { + "name": "anonymous", + "actions": [ "Read"] } ] } From b0025b4ec2fac86193ac3ef3b80839cabc164554 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 17 Jan 2026 11:31:16 +0700 Subject: [PATCH 18/18] feat: Storage bucket prod setup --- .github/workflows/deploy.yml | 12 ++++++++++++ docker-compose.dev.yml | 2 +- docker-compose.prod.yml | 6 +++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a66e6d5..1f6d59c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -72,6 +72,18 @@ jobs: echo "${{ secrets.VITE_ENV_FILE }}" > .env.production cat .env.production + - name: Create seaweedfs config + env: + BUCKET_ACCESS_KEY: ${{ secrets.BUCKET_ACCESS_KEY }} + BUCKET_SECRET_KEY: ${{ secrets.BUCKET_SECRET_KEY }} + run: | + echo "Generating seaweedfs config" + ls -R + mkdir -p seaweedfs_config + envsubst < seaweedfs_config/s3_config.template.json > seaweedfs_config/s3_config.json + echo "Seaweedfs config generated" + cat seaweedfs_config/s3_config.json + - name: Restart docker run: | echo "Restarting docker" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9d6d275..59a33f2 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -45,7 +45,7 @@ services: container_name: cl_bucket_dev restart: always ports: - - "8334:8333" + - "127.0.0.1:8334:8333" - "127.0.0.1:8889:8888" command: "server -s3 -filer -s3.port=8333 -filer.port=8888 -master.volumeSizeLimitMB=1024 -volume.max=100 -s3.config=/etc/seaweedfs/s3_config.json" volumes: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index d7eaac4..dbf2934 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -43,11 +43,11 @@ services: seaweedfs: image: chrislusf/seaweedfs - container_name: cl_bucket_production + container_name: cl_bucket_prod restart: always ports: - - "8333:8333" - - "8888:8888" + - "127.0.0.1:8333:8333" + - "127.0.0.1:8888:8888" command: "server -s3 -filer -s3.port=8333 -filer.port=8888 -master.volumeSizeLimitMB=1024 volume.max=100" volumes: - seaweed_data_prod:/data