From a6bef10a1835c3350a418cf69113a6ea3ab73732 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Tue, 20 Jan 2026 16:49:14 +0700 Subject: [PATCH 01/14] feat: login page desain --- Lb-web/package-lock.json | 129 +++++++++++++++++++++++++++- Lb-web/package.json | 3 + Lb-web/src/App.tsx | 9 -- Lb-web/src/components/ui/button.tsx | 56 ++++++++++++ Lb-web/src/components/ui/card.tsx | 79 +++++++++++++++++ Lb-web/src/components/ui/input.tsx | 22 +++++ Lb-web/src/components/ui/label.tsx | 24 ++++++ Lb-web/src/features/Login/login.tsx | 61 +++++++++++++ Lb-web/src/index.css | 21 +++++ Lb-web/src/main.tsx | 12 ++- 10 files changed, 402 insertions(+), 14 deletions(-) delete mode 100644 Lb-web/src/App.tsx create mode 100644 Lb-web/src/components/ui/button.tsx create mode 100644 Lb-web/src/components/ui/card.tsx create mode 100644 Lb-web/src/components/ui/input.tsx create mode 100644 Lb-web/src/components/ui/label.tsx create mode 100644 Lb-web/src/features/Login/login.tsx diff --git a/Lb-web/package-lock.json b/Lb-web/package-lock.json index 87e67ea..df106c8 100644 --- a/Lb-web/package-lock.json +++ b/Lb-web/package-lock.json @@ -8,12 +8,15 @@ "name": "lb-web", "version": "0.0.0", "dependencies": { + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router": "^7.12.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18" }, @@ -943,6 +946,85 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", @@ -1785,7 +1867,7 @@ "version": "19.2.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1795,7 +1877,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -2315,6 +2397,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2334,7 +2429,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -3500,6 +3595,28 @@ "react": "^19.2.3" } }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3570,6 +3687,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/Lb-web/package.json b/Lb-web/package.json index 92e0666..1e2061e 100644 --- a/Lb-web/package.json +++ b/Lb-web/package.json @@ -10,12 +10,15 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router": "^7.12.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18" }, diff --git a/Lb-web/src/App.tsx b/Lb-web/src/App.tsx deleted file mode 100644 index e677b9b..0000000 --- a/Lb-web/src/App.tsx +++ /dev/null @@ -1,9 +0,0 @@ -function App() { - return ( - <> - test - - ) -} - -export default App diff --git a/Lb-web/src/components/ui/button.tsx b/Lb-web/src/components/ui/button.tsx new file mode 100644 index 0000000..f22ed06 --- /dev/null +++ b/Lb-web/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/Lb-web/src/components/ui/card.tsx b/Lb-web/src/components/ui/card.tsx new file mode 100644 index 0000000..cedfc91 --- /dev/null +++ b/Lb-web/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/Lb-web/src/components/ui/input.tsx b/Lb-web/src/components/ui/input.tsx new file mode 100644 index 0000000..2bd6a9f --- /dev/null +++ b/Lb-web/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/Lb-web/src/components/ui/label.tsx b/Lb-web/src/components/ui/label.tsx new file mode 100644 index 0000000..ef090dc --- /dev/null +++ b/Lb-web/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/Lb-web/src/features/Login/login.tsx b/Lb-web/src/features/Login/login.tsx new file mode 100644 index 0000000..04cd75f --- /dev/null +++ b/Lb-web/src/features/Login/login.tsx @@ -0,0 +1,61 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function Login() { + return ( +
+ {/* Dynamic Background Elements */} +
+
+
+ + + + + Welcome back + + + Enter your email to sign in to your account + + + +
+ + +
+
+ + +
+
+ + + +
+
+ ); +} diff --git a/Lb-web/src/index.css b/Lb-web/src/index.css index 9d01a7b..0553fb4 100644 --- a/Lb-web/src/index.css +++ b/Lb-web/src/index.css @@ -42,6 +42,26 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + + --animate-blob: blob 7s infinite; + + @keyframes blob { + 0% { + transform: translate(0px, 0px) scale(1); + } + + 33% { + transform: translate(30px, -50px) scale(1.1); + } + + 66% { + transform: translate(-20px, 20px) scale(0.9); + } + + 100% { + transform: translate(0px, 0px) scale(1); + } + } } :root { @@ -117,6 +137,7 @@ * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; } diff --git a/Lb-web/src/main.tsx b/Lb-web/src/main.tsx index bef5202..c2cdf4f 100644 --- a/Lb-web/src/main.tsx +++ b/Lb-web/src/main.tsx @@ -1,10 +1,18 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' -import App from './App.tsx' +import Login from './features/Login/login.tsx' +import { createBrowserRouter, RouterProvider } from 'react-router' + +const router = createBrowserRouter([ + { + path: '/login', + element: + } +]) createRoot(document.getElementById('root')!).render( - + , ) From 585c5d787e767026fac7560f036ce0f7c044753a Mon Sep 17 00:00:00 2001 From: Arjuna Date: Wed, 21 Jan 2026 10:42:48 +0700 Subject: [PATCH 02/14] feat: user repo, service, handler, & route added --- Internal/adapters/repository/postgres.go | 49 ++++++++++++++++++++++++ Internal/adapters/repository/userRepo.go | 30 +++++++++++++++ Internal/api/Routes/routev1.go | 20 ++++++++++ Internal/api/handlers/userHandler.go | 37 ++++++++++++++++++ Internal/api/middleware/auth.go | 16 ++++++++ Internal/core/domain/userdb.go | 13 +++++++ Internal/core/services/userService.go | 24 ++++++++++++ cmd/server/main.go | 30 ++++++++++++++- go.mod | 18 +++++---- go.sum | 21 ++++++++-- 10 files changed, 244 insertions(+), 14 deletions(-) create mode 100644 Internal/adapters/repository/postgres.go create mode 100644 Internal/adapters/repository/userRepo.go create mode 100644 Internal/api/Routes/routev1.go create mode 100644 Internal/api/handlers/userHandler.go create mode 100644 Internal/api/middleware/auth.go create mode 100644 Internal/core/domain/userdb.go create mode 100644 Internal/core/services/userService.go diff --git a/Internal/adapters/repository/postgres.go b/Internal/adapters/repository/postgres.go new file mode 100644 index 0000000..bc02520 --- /dev/null +++ b/Internal/adapters/repository/postgres.go @@ -0,0 +1,49 @@ +package repository + +import ( + "fmt" + "log" + + "github.com/Arjuna-Ragil/Localbase/Internal/config" + "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type DBContainer struct { + Gorm *gorm.DB + Sqlx *sqlx.DB +} + +func ConnectDB() (*DBContainer, error) { + cfg := config.LoadConfig() + + gormDB, err := gorm.Open(postgres.Open(cfg.DBURL), &gorm.Config{}) + if err != nil { + log.Fatalf("Failed to connect to gorm database: %v", err) + } + fmt.Println("Connected to gorm database") + + sqlxDB, err := sqlx.Connect("postgres", cfg.DBURL) + if err != nil { + log.Fatalf("Failed to connect to sqlx database: %v", err) + } + fmt.Println("Connected to sqlx database") + + return &DBContainer{ + Gorm: gormDB, + Sqlx: sqlxDB, + }, nil +} +func (db *DBContainer) Migrate() error { + err := db.Gorm.AutoMigrate( + &domain.User{}, + ) + if err != nil { + log.Fatalf("Failed to migrate users: %v", err) + } + fmt.Println("Migrated users") + return nil +} diff --git a/Internal/adapters/repository/userRepo.go b/Internal/adapters/repository/userRepo.go new file mode 100644 index 0000000..db3fe87 --- /dev/null +++ b/Internal/adapters/repository/userRepo.go @@ -0,0 +1,30 @@ +package repository + +import "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + +type UserQuery interface { + Save(user *domain.User) error + FindById(id string) (*domain.User, error) +} +type UserRepository struct { + db *DBContainer +} + +func NewUserRepository(db *DBContainer) *UserRepository { + return &UserRepository{db: db} +} + +// 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 != nil { + return nil, err + } + return &user, nil +} diff --git a/Internal/api/Routes/routev1.go b/Internal/api/Routes/routev1.go new file mode 100644 index 0000000..52b7bd4 --- /dev/null +++ b/Internal/api/Routes/routev1.go @@ -0,0 +1,20 @@ +package Routes + +import ( + "github.com/Arjuna-Ragil/Localbase/Internal/api/handlers" + "github.com/gin-gonic/gin" +) + +type Deps struct { + User *handlers.UserHandler +} + +func SetupRouterV1(r *gin.Engine, deps Deps) { + api := r.Group("/api/v1") + { + user := api.Group("/user") + { + user.GET("/me", deps.User.GetUserHandler) + } + } +} diff --git a/Internal/api/handlers/userHandler.go b/Internal/api/handlers/userHandler.go new file mode 100644 index 0000000..897e371 --- /dev/null +++ b/Internal/api/handlers/userHandler.go @@ -0,0 +1,37 @@ +package handlers + +import ( + "github.com/Arjuna-Ragil/Localbase/Internal/core/services" + "github.com/gin-gonic/gin" +) + +type UserHandler struct { + Service *services.UserService +} + +func NewUserHandler(s *services.UserService) *UserHandler { + return &UserHandler{Service: s} +} + +func (uh *UserHandler) GetUserHandler(c *gin.Context) { + userID, exist := c.Get("userID") + if !exist { + c.JSON(400, gin.H{ + "message": "Unable to find user", + "data": exist, + }) + return + } + user, err := uh.Service.GetUser(userID.(uint)) + if err != nil { + c.JSON(400, gin.H{ + "message": "Unable to get user data", + "data": err, + }) + return + } + c.JSON(200, gin.H{ + "message": "Successfully get user data", + "data": user, + }) +} diff --git a/Internal/api/middleware/auth.go b/Internal/api/middleware/auth.go new file mode 100644 index 0000000..07485f8 --- /dev/null +++ b/Internal/api/middleware/auth.go @@ -0,0 +1,16 @@ +package middleware + +import ( + "os" + + "github.com/gin-gonic/gin" +) + +func AuthMiddleware(c *gin.Context) { + authMode := os.Getenv("AUTH_MODE") + if authMode == "false" { + c.Set("userID", 1) + c.Next() + return + } +} diff --git a/Internal/core/domain/userdb.go b/Internal/core/domain/userdb.go new file mode 100644 index 0000000..94b5b40 --- /dev/null +++ b/Internal/core/domain/userdb.go @@ -0,0 +1,13 @@ +package domain + +import "time" + +type User struct { + Id uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` + Email string `gorm:"size:255;unique;not null" json:"email"` + Password string `gorm:"size:255;not null" json:"password"` + 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"` +} diff --git a/Internal/core/services/userService.go b/Internal/core/services/userService.go new file mode 100644 index 0000000..60d7146 --- /dev/null +++ b/Internal/core/services/userService.go @@ -0,0 +1,24 @@ +package services + +import ( + "errors" + + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" +) + +type UserService struct { + Repo *repository.UserRepository +} + +func NewUserService(repo *repository.UserRepository) *UserService { + return &UserService{Repo: repo} +} + +func (us *UserService) GetUser(id uint) (*domain.User, error) { + user, err := us.Repo.FindById(id) + if err != nil { + return nil, errors.New("user not found") + } + return user, nil +} diff --git a/cmd/server/main.go b/cmd/server/main.go index ca2a26b..e830be7 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -4,29 +4,55 @@ import ( "fmt" "log" + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/api/Routes" + "github.com/Arjuna-Ragil/Localbase/Internal/api/handlers" "github.com/Arjuna-Ragil/Localbase/Internal/config" + "github.com/Arjuna-Ragil/Localbase/Internal/core/services" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func main() { cfg := config.LoadConfig() - fmt.Println("Starting Localbase at port:", cfg.Port) if cfg.AppEnv == "production" { gin.SetMode(gin.ReleaseMode) } + + db, err := repository.ConnectDB() + if err != nil { + log.Fatalf("Error connecting to database: %v", err) + } + if err := db.Migrate(); err != nil { + log.Fatalf("error migrating database: %v", err) + } + r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://localhost:5173"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"}, AllowCredentials: true, })) + deps := SetupApp(db) + + Routes.SetupRouterV1(r, deps) + serverPath := fmt.Sprintf(":%s", cfg.Port) if err := r.Run(serverPath); err != nil { log.Fatalf("Failed to start server: %v", err) } } + +func SetupApp(db *repository.DBContainer) Routes.Deps { + userRepo := repository.NewUserRepository(db) + userService := services.NewUserService(userRepo) + userHandler := handlers.NewUserHandler(userService) + + return Routes.Deps{ + User: userHandler, + } +} diff --git a/go.mod b/go.mod index 03e78ec..61d766d 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,23 @@ module github.com/Arjuna-Ragil/Localbase go 1.25.4 +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.11.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 +) + require ( github.com/bytedance/gopkg v0.1.3 // indirect 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/gabriel-vasile/mimetype v1.4.12 // indirect - github.com/gin-contrib/cors v1.7.6 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.11.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 @@ -22,11 +30,9 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -38,13 +44,9 @@ require ( go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/crypto v0.47.0 // indirect - golang.org/x/mod v0.32.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.41.0 // indirect google.golang.org/protobuf v1.36.11 // indirect - gorm.io/driver/postgres v1.6.0 // indirect - gorm.io/gorm v1.31.1 // indirect ) diff --git a/go.sum b/go.sum index cfe559e..2263bed 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= @@ -7,6 +9,7 @@ github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCc github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 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/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= @@ -16,16 +19,22 @@ 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-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= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -39,6 +48,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 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= @@ -51,6 +62,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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= @@ -58,6 +71,7 @@ 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/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= @@ -73,6 +87,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/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= @@ -83,8 +99,6 @@ golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= @@ -94,12 +108,11 @@ golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= From c1101c1b7502e0dae4cc893ebe37aa84d563f933 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Wed, 21 Jan 2026 11:12:29 +0700 Subject: [PATCH 03/14] chore: CI workflow --- .github/workflows/CI-checks.yml | 63 +++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/CI-checks.yml diff --git a/.github/workflows/CI-checks.yml b/.github/workflows/CI-checks.yml new file mode 100644 index 0000000..c24a9bc --- /dev/null +++ b/.github/workflows/CI-checks.yml @@ -0,0 +1,63 @@ +name: CI integrity checks + +on: + pull_request: + branches: + - main + - dev + push: + branches: + - main + +jobs: + backend-checks: + name: Backend integrity checks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.25' + cache: true + + - name: Download Dependencies + run: go mod download + + - name: Verify Backend Build + run: go build -v ./... + + - name: Run Tests + run: go test -v ./... + + frontend-checks: + name: Frontend integrity checks + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./lb-web + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: lb-web/package-lock.json + + - name: Install Dependencies + run: npm ci + + - name: Type check & lint + run: npm run lint + continue-on-error: true + + - name: Run Tests + run: npm run test + + - name: Build + run: npm run build \ No newline at end of file From 85e00c03e2ea59c3bb25e681006d48685cfb14c6 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Wed, 21 Jan 2026 13:19:21 +0700 Subject: [PATCH 04/14] feat: register system added --- Internal/adapters/repository/authRepo.go | 18 +++++++++++ Internal/api/Routes/routev1.go | 6 ++++ Internal/api/handlers/authHandler.go | 38 ++++++++++++++++++++++++ Internal/core/domain/userdb.go | 20 +++++++++++-- Internal/core/services/authService.go | 35 ++++++++++++++++++++++ cmd/server/main.go | 5 ++++ 6 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 Internal/adapters/repository/authRepo.go create mode 100644 Internal/api/handlers/authHandler.go create mode 100644 Internal/core/services/authService.go diff --git a/Internal/adapters/repository/authRepo.go b/Internal/adapters/repository/authRepo.go new file mode 100644 index 0000000..9c3cfbd --- /dev/null +++ b/Internal/adapters/repository/authRepo.go @@ -0,0 +1,18 @@ +package repository + +import "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + +type AuthRepo struct { + Repo *DBContainer +} + +func NewAuthRepo(repo *DBContainer) *AuthRepo { + return &AuthRepo{Repo: repo} +} + +func (ar *AuthRepo) RegisterRepo(user *domain.User) error { + if err := ar.Repo.Gorm.Create(user).Error; err != nil { + return err + } + return nil +} diff --git a/Internal/api/Routes/routev1.go b/Internal/api/Routes/routev1.go index 52b7bd4..6017634 100644 --- a/Internal/api/Routes/routev1.go +++ b/Internal/api/Routes/routev1.go @@ -7,6 +7,7 @@ import ( type Deps struct { User *handlers.UserHandler + Auth *handlers.AuthHandler } func SetupRouterV1(r *gin.Engine, deps Deps) { @@ -16,5 +17,10 @@ func SetupRouterV1(r *gin.Engine, deps Deps) { { user.GET("/me", deps.User.GetUserHandler) } + + auth := api.Group("/auth") + { + auth.POST("/Register", deps.Auth.RegisterHandler) + } } } diff --git a/Internal/api/handlers/authHandler.go b/Internal/api/handlers/authHandler.go new file mode 100644 index 0000000..43d8fe4 --- /dev/null +++ b/Internal/api/handlers/authHandler.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "net/http" + + "github.com/Arjuna-Ragil/Localbase/Internal/core/services" + "github.com/gin-gonic/gin" +) + +type AuthHandler struct { + handler *services.AuthService +} + +func NewAuthHandler(handler *services.AuthService) *AuthHandler { + return &AuthHandler{handler: handler} +} + +func (ah *AuthHandler) RegisterHandler(c *gin.Context) { + var input services.RegisterInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Invalid Input", + "data": err.Error(), + }) + return + } + if err := ah.handler.RegisterService(&input); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": "Register service failed", + "data": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "message": "Register service success", + "data": nil, + }) +} diff --git a/Internal/core/domain/userdb.go b/Internal/core/domain/userdb.go index 94b5b40..bb84290 100644 --- a/Internal/core/domain/userdb.go +++ b/Internal/core/domain/userdb.go @@ -1,13 +1,29 @@ package domain -import "time" +import ( + "time" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) type User struct { Id uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"` Email string `gorm:"size:255;unique;not null" json:"email"` - Password string `gorm:"size:255;not null" json:"password"` + 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"` } + +func (u *User) BeforeCreate(tx *gorm.DB) (err error) { + if len(u.Password) < 50 { + hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + u.Password = string(hashed) + } + return nil +} diff --git a/Internal/core/services/authService.go b/Internal/core/services/authService.go new file mode 100644 index 0000000..5f3bf33 --- /dev/null +++ b/Internal/core/services/authService.go @@ -0,0 +1,35 @@ +package services + +import ( + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" +) + +type AuthService struct { + Service *repository.AuthRepo +} + +type RegisterInput struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + Role string `json:"role" binding:"required"` +} + +func NewAuthService(service *repository.AuthRepo) *AuthService { + return &AuthService{Service: service} +} + +func (as *AuthService) RegisterService(input *RegisterInput) error { + user := domain.User{ + Username: input.Username, + Email: input.Email, + Password: input.Password, + Role: input.Role, + } + err := as.Service.RegisterRepo(&user) + if err != nil { + return err + } + return nil +} diff --git a/cmd/server/main.go b/cmd/server/main.go index e830be7..ef78321 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -52,7 +52,12 @@ func SetupApp(db *repository.DBContainer) Routes.Deps { userService := services.NewUserService(userRepo) userHandler := handlers.NewUserHandler(userService) + authRepo := repository.NewAuthRepo(db) + authService := services.NewAuthService(authRepo) + authHandler := handlers.NewAuthHandler(authService) + return Routes.Deps{ User: userHandler, + Auth: authHandler, } } From 78557560136649a7016b975dc5e718ea264bb09c Mon Sep 17 00:00:00 2001 From: Arjuna Date: Wed, 21 Jan 2026 14:35:53 +0700 Subject: [PATCH 05/14] feat: Login and jwt added --- Internal/adapters/repository/authRepo.go | 16 ++++++--- Internal/api/Routes/routev1.go | 2 ++ Internal/api/handlers/authHandler.go | 41 +++++++++++++++++++++--- Internal/config/config.go | 2 ++ Internal/core/services/authService.go | 39 +++++++++++++++++++--- go.mod | 1 + go.sum | 2 ++ 7 files changed, 91 insertions(+), 12 deletions(-) diff --git a/Internal/adapters/repository/authRepo.go b/Internal/adapters/repository/authRepo.go index 9c3cfbd..d4c80fb 100644 --- a/Internal/adapters/repository/authRepo.go +++ b/Internal/adapters/repository/authRepo.go @@ -3,16 +3,24 @@ package repository import "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" type AuthRepo struct { - Repo *DBContainer + DB *DBContainer } -func NewAuthRepo(repo *DBContainer) *AuthRepo { - return &AuthRepo{Repo: repo} +func NewAuthRepo(db *DBContainer) *AuthRepo { + return &AuthRepo{DB: db} } func (ar *AuthRepo) RegisterRepo(user *domain.User) error { - if err := ar.Repo.Gorm.Create(user).Error; err != nil { + if err := ar.DB.Gorm.Create(user).Error; err != nil { return err } return nil } + +func (ar *AuthRepo) LoginRepo(email string) (*domain.User, error) { + var user domain.User + if err := ar.DB.Gorm.Where("email = ?", email).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} diff --git a/Internal/api/Routes/routev1.go b/Internal/api/Routes/routev1.go index 6017634..e25f183 100644 --- a/Internal/api/Routes/routev1.go +++ b/Internal/api/Routes/routev1.go @@ -21,6 +21,8 @@ func SetupRouterV1(r *gin.Engine, deps Deps) { auth := api.Group("/auth") { auth.POST("/Register", deps.Auth.RegisterHandler) + auth.POST("/Login", deps.Auth.LoginHandler) + auth.POST("/logout", deps.Auth.LogoutHandler) } } } diff --git a/Internal/api/handlers/authHandler.go b/Internal/api/handlers/authHandler.go index 43d8fe4..c6a3932 100644 --- a/Internal/api/handlers/authHandler.go +++ b/Internal/api/handlers/authHandler.go @@ -8,11 +8,11 @@ import ( ) type AuthHandler struct { - handler *services.AuthService + Service *services.AuthService } -func NewAuthHandler(handler *services.AuthService) *AuthHandler { - return &AuthHandler{handler: handler} +func NewAuthHandler(service *services.AuthService) *AuthHandler { + return &AuthHandler{Service: service} } func (ah *AuthHandler) RegisterHandler(c *gin.Context) { @@ -24,7 +24,7 @@ func (ah *AuthHandler) RegisterHandler(c *gin.Context) { }) return } - if err := ah.handler.RegisterService(&input); err != nil { + if err := ah.Service.RegisterService(&input); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message": "Register service failed", "data": err.Error(), @@ -36,3 +36,36 @@ func (ah *AuthHandler) RegisterHandler(c *gin.Context) { "data": nil, }) } + +func (ah *AuthHandler) LoginHandler(c *gin.Context) { + var input services.LoginInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Invalid Input", + "data": err.Error(), + }) + return + } + tokenString, err := ah.Service.LoginService(&input) + if err != nil { + c.JSON(401, gin.H{ + "message": "Login failed", + "data": err.Error(), + }) + return + } + c.SetSameSite(http.SameSiteLaxMode) + c.SetCookie("authorization", tokenString, 3600*24, "/", "", false, true) + c.JSON(http.StatusOK, gin.H{ + "message": "Login success", + "data": tokenString, + }) +} + +func (ah *AuthHandler) LogoutHandler(c *gin.Context) { + c.SetCookie("authorization", "", -1, "/", "", false, true) + c.JSON(http.StatusOK, gin.H{ + "message": "Logout success", + "data": nil, + }) +} diff --git a/Internal/config/config.go b/Internal/config/config.go index 010aa67..287f473 100644 --- a/Internal/config/config.go +++ b/Internal/config/config.go @@ -12,6 +12,7 @@ type Config struct { Port string DBURL string LedgerPath string + SecretKey string } func LoadConfig() Config { @@ -23,6 +24,7 @@ func LoadConfig() Config { AppEnv: getEnv("APP_ENV", "local"), Port: getEnv("PORT", "8080"), LedgerPath: getEnv("LEDGER_PATH", "../Storage/ledger.db"), + SecretKey: getEnv("SECRET_KEY", "secret"), DBURL: "host=" + getEnv("DB_HOST", "localhost") + " user=" + getEnv("DB_USER", "postgres") + " password=" + getEnv("DB_PASSWORD", "password") + diff --git a/Internal/core/services/authService.go b/Internal/core/services/authService.go index 5f3bf33..848445e 100644 --- a/Internal/core/services/authService.go +++ b/Internal/core/services/authService.go @@ -1,12 +1,17 @@ package services import ( + "time" + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/config" "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" ) type AuthService struct { - Service *repository.AuthRepo + Repo *repository.AuthRepo } type RegisterInput struct { @@ -16,8 +21,13 @@ type RegisterInput struct { Role string `json:"role" binding:"required"` } -func NewAuthService(service *repository.AuthRepo) *AuthService { - return &AuthService{Service: service} +type LoginInput struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` +} + +func NewAuthService(repo *repository.AuthRepo) *AuthService { + return &AuthService{Repo: repo} } func (as *AuthService) RegisterService(input *RegisterInput) error { @@ -27,9 +37,30 @@ func (as *AuthService) RegisterService(input *RegisterInput) error { Password: input.Password, Role: input.Role, } - err := as.Service.RegisterRepo(&user) + err := as.Repo.RegisterRepo(&user) if err != nil { return err } return nil } + +func (as *AuthService) LoginService(input *LoginInput) (string, error) { + cfg := config.LoadConfig() + + user, err := as.Repo.LoginRepo(input.Email) + if err != nil { + return "", err + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil { + return "", err + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": user.Id, + "exp": time.Now().Add(time.Hour * 48).Unix(), + }) + tokenString, err := token.SignedString([]byte(cfg.SecretKey)) + if err != nil { + return "", err + } + return tokenString, nil +} diff --git a/go.mod b/go.mod index 61d766d..23d07ae 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( 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.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.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.8.0 // indirect diff --git a/go.sum b/go.sum index 2263bed..32c3108 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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= From 636f64c502bab83561a517267d4e9da88ffe2535 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Wed, 21 Jan 2026 15:53:05 +0700 Subject: [PATCH 06/14] feat: admin login logic added --- Internal/adapters/repository/sysRepo.go | 19 ++ Internal/api/Routes/routev1.go | 10 +- Internal/api/handlers/sysHandler.go | 24 ++ Internal/core/domain/userdb.go | 10 + Internal/core/services/sysService.go | 19 ++ Lb-web/package-lock.json | 280 ++++++++++++++++++ Lb-web/package.json | 1 + Lb-web/src/features/Login/hooks/useLogin.ts | 44 +++ .../src/features/Login/hooks/useRegister.ts | 58 ++++ Lb-web/src/features/Login/login.tsx | 78 +++-- Lb-web/src/features/Login/register.tsx | 109 +++++++ Lb-web/src/main.tsx | 69 ++++- Lb-web/src/services/api.ts | 8 + cmd/server/main.go | 9 +- 14 files changed, 701 insertions(+), 37 deletions(-) create mode 100644 Internal/adapters/repository/sysRepo.go create mode 100644 Internal/api/handlers/sysHandler.go create mode 100644 Internal/core/services/sysService.go create mode 100644 Lb-web/src/features/Login/hooks/useLogin.ts create mode 100644 Lb-web/src/features/Login/hooks/useRegister.ts create mode 100644 Lb-web/src/features/Login/register.tsx create mode 100644 Lb-web/src/services/api.ts diff --git a/Internal/adapters/repository/sysRepo.go b/Internal/adapters/repository/sysRepo.go new file mode 100644 index 0000000..717c815 --- /dev/null +++ b/Internal/adapters/repository/sysRepo.go @@ -0,0 +1,19 @@ +package repository + +import "github.com/Arjuna-Ragil/Localbase/Internal/core/domain" + +type SystemRepo struct { + DB *DBContainer +} + +func NewSystemRepo(db *DBContainer) *SystemRepo { + return &SystemRepo{DB: db} +} + +func (sr *SystemRepo) IsAppInitialized() (bool, error) { + var count int64 + if err := sr.DB.Gorm.Model(&domain.User{}).Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} diff --git a/Internal/api/Routes/routev1.go b/Internal/api/Routes/routev1.go index e25f183..a235892 100644 --- a/Internal/api/Routes/routev1.go +++ b/Internal/api/Routes/routev1.go @@ -6,8 +6,9 @@ import ( ) type Deps struct { - User *handlers.UserHandler - Auth *handlers.AuthHandler + User *handlers.UserHandler + Auth *handlers.AuthHandler + System *handlers.SystemHandler } func SetupRouterV1(r *gin.Engine, deps Deps) { @@ -24,5 +25,10 @@ func SetupRouterV1(r *gin.Engine, deps Deps) { auth.POST("/Login", deps.Auth.LoginHandler) auth.POST("/logout", deps.Auth.LogoutHandler) } + + system := api.Group("/system") + { + system.GET("/status", deps.System.CheckAppInitStatus) + } } } diff --git a/Internal/api/handlers/sysHandler.go b/Internal/api/handlers/sysHandler.go new file mode 100644 index 0000000..20e54a5 --- /dev/null +++ b/Internal/api/handlers/sysHandler.go @@ -0,0 +1,24 @@ +package handlers + +import ( + "net/http" + + "github.com/Arjuna-Ragil/Localbase/Internal/core/services" + "github.com/gin-gonic/gin" +) + +type SystemHandler struct { + Service *services.SystemService +} + +func NewSystemHandler(service *services.SystemService) *SystemHandler { + return &SystemHandler{Service: service} +} + +func (sh *SystemHandler) CheckAppInitStatus(c *gin.Context) { + isInit, _ := sh.Service.CheckAppInitialized() + c.JSON(http.StatusOK, gin.H{ + "message": "App init status", + "data": isInit, + }) +} diff --git a/Internal/core/domain/userdb.go b/Internal/core/domain/userdb.go index bb84290..110598d 100644 --- a/Internal/core/domain/userdb.go +++ b/Internal/core/domain/userdb.go @@ -25,5 +25,15 @@ func (u *User) BeforeCreate(tx *gorm.DB) (err error) { } u.Password = string(hashed) } + var count int64 + tx.Model(&User{}).Count(&count) + if count == 0 { + u.Role = "admin" + } else { + if u.Role == "" { + u.Role = "user" + } + } + return nil } diff --git a/Internal/core/services/sysService.go b/Internal/core/services/sysService.go new file mode 100644 index 0000000..9242170 --- /dev/null +++ b/Internal/core/services/sysService.go @@ -0,0 +1,19 @@ +package services + +import "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + +type SystemService struct { + Repo *repository.SystemRepo +} + +func NewSystemService(repo *repository.SystemRepo) *SystemService { + return &SystemService{Repo: repo} +} + +func (ss *SystemService) CheckAppInitialized() (bool, error) { + isInitialized, err := ss.Repo.IsAppInitialized() + if err != nil { + return false, err + } + return isInitialized, nil +} diff --git a/Lb-web/package-lock.json b/Lb-web/package-lock.json index df106c8..311c25b 100644 --- a/Lb-web/package-lock.json +++ b/Lb-web/package-lock.json @@ -11,6 +11,7 @@ "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.18", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", @@ -2232,6 +2233,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2294,6 +2312,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2383,6 +2414,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2457,6 +2500,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2466,6 +2518,20 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -2486,6 +2552,51 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -2823,6 +2934,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2837,6 +2984,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2847,6 +3003,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2873,6 +3066,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2889,6 +3094,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -3373,6 +3617,36 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3564,6 +3838,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/Lb-web/package.json b/Lb-web/package.json index 1e2061e..896865c 100644 --- a/Lb-web/package.json +++ b/Lb-web/package.json @@ -13,6 +13,7 @@ "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.18", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", diff --git a/Lb-web/src/features/Login/hooks/useLogin.ts b/Lb-web/src/features/Login/hooks/useLogin.ts new file mode 100644 index 0000000..a254e7d --- /dev/null +++ b/Lb-web/src/features/Login/hooks/useLogin.ts @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import api from '@/services/api'; +import { useNavigate } from 'react-router'; + +export const useLogin = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + await api.post('/auth/Login', { email, password }); + // Redirect or update global auth state here + // For now, assuming successful cookie set by backend + navigate('/dashboard'); // or wherever you want to redirect + console.log("Login successful"); + } catch (err: any) { + console.error("Login failed", err); + if (err.response && err.response.data && err.response.data.message) { + setError(err.response.data.message); + } else { + setError('Something went wrong. Please try again.'); + } + } finally { + setLoading(false); + } + }; + + return { + email, + setEmail, + password, + setPassword, + loading, + error, + handleLogin, + }; +}; diff --git a/Lb-web/src/features/Login/hooks/useRegister.ts b/Lb-web/src/features/Login/hooks/useRegister.ts new file mode 100644 index 0000000..cf3d13e --- /dev/null +++ b/Lb-web/src/features/Login/hooks/useRegister.ts @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import api from '@/services/api'; +import { useNavigate } from 'react-router'; + +export const useRegister = () => { + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [role, setRole] = useState('admin'); // Default to admin for first user? Or let backend handle it? + // Assuming for first run, the user being created is likely an Admin/Owner. + // The backend might enforce this or we can just send it. + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + const handleRegister = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + await api.post('/auth/Register', { + username, + email, + password, + role + }); + console.log("Registration successful"); + // After successful registration, usually redirect to login or auto-login. + // For now, redirect to login page (which should now be accessible since system is init) + navigate('/login'); + } catch (err: any) { + console.error("Registration failed", err); + if (err.response && err.response.data && err.response.data.message) { + setError(err.response.data.message); + } else { + setError('Something went wrong. Please try again.'); + } + } finally { + setLoading(false); + } + }; + + return { + username, + setUsername, + email, + setEmail, + password, + setPassword, + role, + setRole, + loading, + error, + handleRegister, + }; +}; diff --git a/Lb-web/src/features/Login/login.tsx b/Lb-web/src/features/Login/login.tsx index 04cd75f..a414a78 100644 --- a/Lb-web/src/features/Login/login.tsx +++ b/Lb-web/src/features/Login/login.tsx @@ -9,8 +9,11 @@ import { } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { useLogin } from "./hooks/useLogin"; export default function Login() { + const { email, setEmail, password, setPassword, loading, error, handleLogin } = useLogin(); + return (
{/* Dynamic Background Elements */} @@ -18,7 +21,7 @@ export default function Login() {
- + Welcome back @@ -27,34 +30,51 @@ export default function Login() { Enter your email to sign in to your account - -
- - -
-
- - -
-
- - - +
+ + {error && ( +
+ {error} +
+ )} +
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + + +
); diff --git a/Lb-web/src/features/Login/register.tsx b/Lb-web/src/features/Login/register.tsx new file mode 100644 index 0000000..7329f03 --- /dev/null +++ b/Lb-web/src/features/Login/register.tsx @@ -0,0 +1,109 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useRegister } from "./hooks/useRegister"; + +export default function Register() { + const { + username, setUsername, + email, setEmail, + password, setPassword, + role, setRole, + loading, error, handleRegister + } = useRegister(); + + return ( +
+ {/* Dynamic Background Elements */} +
+
+
+ + + + + Create an Account + + + Setup your initial administrator account + + +
+ + {error && ( +
+ {error} +
+ )} +
+ + setUsername(e.target.value)} + required + /> +
+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ {/* Hidden Role Input - defaulting to admin/input value */} +
+ setRole(e.target.value)} + /> +
+ +
+ + + +
+
+
+ ); +} diff --git a/Lb-web/src/main.tsx b/Lb-web/src/main.tsx index c2cdf4f..4ca7ef5 100644 --- a/Lb-web/src/main.tsx +++ b/Lb-web/src/main.tsx @@ -1,13 +1,74 @@ -import { StrictMode } from 'react' +import { StrictMode, useEffect, useState } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import Login from './features/Login/login.tsx' -import { createBrowserRouter, RouterProvider } from 'react-router' +import Register from './features/Login/register.tsx' +import { createBrowserRouter, RouterProvider, Navigate, Outlet } from 'react-router' +import api from './services/api.ts' + +// Protected Route Component to Handle First-Run Check +const Root = () => { + const [loading, setLoading] = useState(true); + const [isInitialized, setIsInitialized] = useState(null); + + useEffect(() => { + const checkStatus = async () => { + try { + const response = await api.get('/system/status'); + setIsInitialized(response.data.data); + } catch (error) { + console.error("Failed to check system status", error); + // Fallback: Assume initialized to avoid stuck on loading, or show error + setIsInitialized(true); + } finally { + setLoading(false); + } + }; + checkStatus(); + }, []); + + if (loading) { + return
Loading...
; + } + + // If not initialized, force redirect to register + if (isInitialized === false && window.location.pathname !== '/register') { + return ; + } + + // If initialized and trying to access register (optional: prevent re-registration) + if (isInitialized === true && window.location.pathname === '/register') { + return ; + } + + // Default redirect to login if root + if (window.location.pathname === '/') { + return ; + } + + + return ; +}; const router = createBrowserRouter([ { - path: '/login', - element: + path: '/', + element: , + children: [ + { + path: 'login', + element: + }, + { + path: 'register', + element: + }, + { + // Placeholder dashboard route + path: 'dashboard', + element:
Dashboard (Protected)
+ } + ] } ]) diff --git a/Lb-web/src/services/api.ts b/Lb-web/src/services/api.ts new file mode 100644 index 0000000..01952c9 --- /dev/null +++ b/Lb-web/src/services/api.ts @@ -0,0 +1,8 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: 'http://localhost:8080/api/v1', + withCredentials: true, // Important for cookies +}); + +export default api; diff --git a/cmd/server/main.go b/cmd/server/main.go index ef78321..066879f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -56,8 +56,13 @@ func SetupApp(db *repository.DBContainer) Routes.Deps { authService := services.NewAuthService(authRepo) authHandler := handlers.NewAuthHandler(authService) + systemRepo := repository.NewSystemRepo(db) + systemService := services.NewSystemService(systemRepo) + systemHandler := handlers.NewSystemHandler(systemService) + return Routes.Deps{ - User: userHandler, - Auth: authHandler, + User: userHandler, + Auth: authHandler, + System: systemHandler, } } From 025089781d3d71d671e2c74a70108d71fb263455 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Wed, 21 Jan 2026 17:02:30 +0700 Subject: [PATCH 07/14] feat: Home page desain added --- Lb-web/public/Localbase Logo.svg | 8 +++++ Lb-web/public/vite.svg | 1 - Lb-web/src/features/Login/login.tsx | 3 ++ Lb-web/src/features/Login/register.tsx | 3 ++ Lb-web/src/main.tsx | 11 ++++-- Lb-web/src/pages/Home.tsx | 50 ++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 Lb-web/public/Localbase Logo.svg delete mode 100644 Lb-web/public/vite.svg create mode 100644 Lb-web/src/pages/Home.tsx diff --git a/Lb-web/public/Localbase Logo.svg b/Lb-web/public/Localbase Logo.svg new file mode 100644 index 0000000..b56ef46 --- /dev/null +++ b/Lb-web/public/Localbase Logo.svg @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/Lb-web/public/vite.svg b/Lb-web/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/Lb-web/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Lb-web/src/features/Login/login.tsx b/Lb-web/src/features/Login/login.tsx index a414a78..03d830e 100644 --- a/Lb-web/src/features/Login/login.tsx +++ b/Lb-web/src/features/Login/login.tsx @@ -23,6 +23,9 @@ export default function Login() { +
+ Localbase Logo +
Welcome back diff --git a/Lb-web/src/features/Login/register.tsx b/Lb-web/src/features/Login/register.tsx index 7329f03..b692fce 100644 --- a/Lb-web/src/features/Login/register.tsx +++ b/Lb-web/src/features/Login/register.tsx @@ -29,6 +29,9 @@ export default function Register() { +
+ Localbase Logo +
Create an Account diff --git a/Lb-web/src/main.tsx b/Lb-web/src/main.tsx index 4ca7ef5..c14f38a 100644 --- a/Lb-web/src/main.tsx +++ b/Lb-web/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import './index.css' import Login from './features/Login/login.tsx' import Register from './features/Login/register.tsx' +import Home from './pages/Home.tsx' import { createBrowserRouter, RouterProvider, Navigate, Outlet } from 'react-router' import api from './services/api.ts' @@ -42,9 +43,9 @@ const Root = () => { } // Default redirect to login if root - if (window.location.pathname === '/') { - return ; - } + // if (window.location.pathname === '/') { + // return ; + // } return ; @@ -63,6 +64,10 @@ const router = createBrowserRouter([ path: 'register', element: }, + { + index: true, + element: + }, { // Placeholder dashboard route path: 'dashboard', diff --git a/Lb-web/src/pages/Home.tsx b/Lb-web/src/pages/Home.tsx new file mode 100644 index 0000000..15364d5 --- /dev/null +++ b/Lb-web/src/pages/Home.tsx @@ -0,0 +1,50 @@ +import { Button } from "@/components/ui/button"; +import { User, Settings, Plus } from "lucide-react"; + +export default function Home() { + return ( +
+ {/* Subtle Grid & Gradient Background */} +
+ +
+
+
+ + {/* Navbar */} +
+
+ Localbase Logo +

Localbase

+
+
+ + +
+
+ + {/* Main Content */} +
+
+
+
+ +
+

There is no Project yet

+

Create your first project to get started with Localbase.

+ +
+
+
+
+ ); +} From 1e505f5108cb611245480666f4a5df9b2de66a55 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Wed, 21 Jan 2026 20:06:37 +0700 Subject: [PATCH 08/14] feat: auth middleware added --- Internal/api/Routes/routev1.go | 25 ++++-- Internal/api/middleware/auth.go | 86 +++++++++++++++++-- Internal/config/config.go | 2 + .../Login/components/AuthWrappers.tsx | 30 +++++++ .../features/Login/context/AuthContext.tsx | 59 +++++++++++++ Lb-web/src/features/Login/hooks/useLogin.ts | 6 +- Lb-web/src/main.tsx | 67 +++------------ Lb-web/src/pages/Home.tsx | 48 +++++++++-- Lb-web/src/services/api.ts | 8 ++ cmd/server/main.go | 12 +-- 10 files changed, 260 insertions(+), 83 deletions(-) create mode 100644 Lb-web/src/features/Login/components/AuthWrappers.tsx create mode 100644 Lb-web/src/features/Login/context/AuthContext.tsx diff --git a/Internal/api/Routes/routev1.go b/Internal/api/Routes/routev1.go index a235892..db257b4 100644 --- a/Internal/api/Routes/routev1.go +++ b/Internal/api/Routes/routev1.go @@ -1,22 +1,29 @@ package Routes import ( + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" "github.com/Arjuna-Ragil/Localbase/Internal/api/handlers" + "github.com/Arjuna-Ragil/Localbase/Internal/api/middleware" + "github.com/Arjuna-Ragil/Localbase/Internal/config" "github.com/gin-gonic/gin" ) type Deps struct { - User *handlers.UserHandler - Auth *handlers.AuthHandler - System *handlers.SystemHandler + User *handlers.UserHandler + Auth *handlers.AuthHandler + System *handlers.SystemHandler + UserRepo *repository.UserRepository + Config *config.Config } func SetupRouterV1(r *gin.Engine, deps Deps) { api := r.Group("/api/v1") + + authMiddleware := middleware.AuthMiddleware(deps.UserRepo, deps.Config) { - user := api.Group("/user") + system := api.Group("/system") { - user.GET("/me", deps.User.GetUserHandler) + system.GET("/status", deps.System.CheckAppInitStatus) } auth := api.Group("/auth") @@ -26,9 +33,13 @@ func SetupRouterV1(r *gin.Engine, deps Deps) { auth.POST("/logout", deps.Auth.LogoutHandler) } - system := api.Group("/system") + protected := api.Group("/protected") + protected.Use(authMiddleware) { - system.GET("/status", deps.System.CheckAppInitStatus) + user := protected.Group("/user") + { + user.GET("/me", deps.User.GetUserHandler) + } } } } diff --git a/Internal/api/middleware/auth.go b/Internal/api/middleware/auth.go index 07485f8..d274e69 100644 --- a/Internal/api/middleware/auth.go +++ b/Internal/api/middleware/auth.go @@ -1,16 +1,88 @@ package middleware import ( - "os" + "fmt" + "strings" + "time" + "github.com/Arjuna-Ragil/Localbase/Internal/adapters/repository" + "github.com/Arjuna-Ragil/Localbase/Internal/config" "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" ) -func AuthMiddleware(c *gin.Context) { - authMode := os.Getenv("AUTH_MODE") - if authMode == "false" { - c.Set("userID", 1) - c.Next() - return +func AuthMiddleware(userRepo *repository.UserRepository, cfg *config.Config) gin.HandlerFunc { + return func(c *gin.Context) { + if cfg.AuthMode == "false" { + c.Set("userID", 1) + c.Next() + return + } + tokenString, err := c.Cookie("authorization") + if tokenString == "" { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(401, gin.H{ + "message": "Authorization header not found", + "data": err.Error(), + }) + return + } + parts := strings.Split(authHeader, " ") + if len(parts) == 2 { + tokenString = parts[1] + } else { + tokenString = authHeader + } + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("method unknown") + } + return []byte(cfg.SecretKey), nil + }) + + if err != nil || !token.Valid { + c.JSON(401, gin.H{ + "message": "Invalid token", + "data": err.Error(), + }) + return + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + if float64(time.Now().Unix()) > claims["exp"].(float64) { + c.JSON(401, gin.H{ + "message": "Token expired", + "data": err.Error(), + }) + return + } + subID := uint(claims["sub"].(float64)) + user, err := userRepo.FindById(subID) + if err != nil { + c.JSON(401, gin.H{ + "message": "User not found", + "data": err.Error(), + }) + return + } + + c.Set("userID", user.Id) + c.Set("userRole", user.Role) + c.Next() + + c.JSON(200, gin.H{ + "message": "Success", + "data": " ", + }) + } else { + c.JSON(401, gin.H{ + "message": "Invalid token", + "data": err.Error(), + }) + return + } } } diff --git a/Internal/config/config.go b/Internal/config/config.go index 287f473..7b19b63 100644 --- a/Internal/config/config.go +++ b/Internal/config/config.go @@ -13,6 +13,7 @@ type Config struct { DBURL string LedgerPath string SecretKey string + AuthMode string } func LoadConfig() Config { @@ -25,6 +26,7 @@ func LoadConfig() Config { Port: getEnv("PORT", "8080"), LedgerPath: getEnv("LEDGER_PATH", "../Storage/ledger.db"), SecretKey: getEnv("SECRET_KEY", "secret"), + AuthMode: getEnv("AUTH_MODE", "true"), DBURL: "host=" + getEnv("DB_HOST", "localhost") + " user=" + getEnv("DB_USER", "postgres") + " password=" + getEnv("DB_PASSWORD", "password") + diff --git a/Lb-web/src/features/Login/components/AuthWrappers.tsx b/Lb-web/src/features/Login/components/AuthWrappers.tsx new file mode 100644 index 0000000..cca6571 --- /dev/null +++ b/Lb-web/src/features/Login/components/AuthWrappers.tsx @@ -0,0 +1,30 @@ +import { Navigate, Outlet } from "react-router"; +import { useAuth } from "../context/AuthContext"; + +export const ProtectedRoute = () => { + const { isAuthenticated, loading } = useAuth(); + + if (loading) { + return
Loading...
; + } + + if (!isAuthenticated) { + return ; + } + + return ; +}; + +export const PublicRoute = () => { + const { isAuthenticated, loading } = useAuth(); + + if (loading) { + return
Loading...
; + } + + if (isAuthenticated) { + return ; + } + + return ; +}; diff --git a/Lb-web/src/features/Login/context/AuthContext.tsx b/Lb-web/src/features/Login/context/AuthContext.tsx new file mode 100644 index 0000000..7c2e2dc --- /dev/null +++ b/Lb-web/src/features/Login/context/AuthContext.tsx @@ -0,0 +1,59 @@ +import { createContext, useContext, useEffect, useState, type ReactNode } from "react"; +import api from "@/services/api"; + +interface AuthContextType { + isAuthenticated: boolean | null; + loading: boolean; + login: () => void; // call this after successful login to update state + logout: () => Promise; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [isAuthenticated, setIsAuthenticated] = useState(null); + const [loading, setLoading] = useState(true); + + const checkAuth = async () => { + try { + await api.get('/protected/user/me'); + setIsAuthenticated(true); + } catch { + setIsAuthenticated(false); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + checkAuth(); + }, []); + + const login = () => { + setIsAuthenticated(true); + }; + + const logout = async () => { + try { + await api.post('/auth/logout'); + } catch (error) { + console.error("Logout failed", error); + } finally { + setIsAuthenticated(false); + } + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/Lb-web/src/features/Login/hooks/useLogin.ts b/Lb-web/src/features/Login/hooks/useLogin.ts index a254e7d..70192eb 100644 --- a/Lb-web/src/features/Login/hooks/useLogin.ts +++ b/Lb-web/src/features/Login/hooks/useLogin.ts @@ -1,13 +1,13 @@ import { useState } from 'react'; import api from '@/services/api'; -import { useNavigate } from 'react-router'; +import { useAuth } from '../context/AuthContext'; export const useLogin = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const navigate = useNavigate(); + const { login } = useAuth(); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); @@ -18,7 +18,7 @@ export const useLogin = () => { await api.post('/auth/Login', { email, password }); // Redirect or update global auth state here // For now, assuming successful cookie set by backend - navigate('/dashboard'); // or wherever you want to redirect + login(); // Update auth state, wrapper will handle redirect console.log("Login successful"); } catch (err: any) { console.error("Login failed", err); diff --git a/Lb-web/src/main.tsx b/Lb-web/src/main.tsx index c14f38a..bd46efe 100644 --- a/Lb-web/src/main.tsx +++ b/Lb-web/src/main.tsx @@ -1,60 +1,16 @@ -import { StrictMode, useEffect, useState } from 'react' +import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import Login from './features/Login/login.tsx' import Register from './features/Login/register.tsx' import Home from './pages/Home.tsx' -import { createBrowserRouter, RouterProvider, Navigate, Outlet } from 'react-router' -import api from './services/api.ts' - -// Protected Route Component to Handle First-Run Check -const Root = () => { - const [loading, setLoading] = useState(true); - const [isInitialized, setIsInitialized] = useState(null); - - useEffect(() => { - const checkStatus = async () => { - try { - const response = await api.get('/system/status'); - setIsInitialized(response.data.data); - } catch (error) { - console.error("Failed to check system status", error); - // Fallback: Assume initialized to avoid stuck on loading, or show error - setIsInitialized(true); - } finally { - setLoading(false); - } - }; - checkStatus(); - }, []); - - if (loading) { - return
Loading...
; - } - - // If not initialized, force redirect to register - if (isInitialized === false && window.location.pathname !== '/register') { - return ; - } - - // If initialized and trying to access register (optional: prevent re-registration) - if (isInitialized === true && window.location.pathname === '/register') { - return ; - } - - // Default redirect to login if root - // if (window.location.pathname === '/') { - // return ; - // } - - - return ; -}; +import { createBrowserRouter, RouterProvider } from 'react-router' +import { AuthProvider } from './features/Login/context/AuthContext.tsx' +import { ProtectedRoute, PublicRoute } from './features/Login/components/AuthWrappers.tsx' const router = createBrowserRouter([ { - path: '/', - element: , + element: , children: [ { path: 'login', @@ -63,15 +19,16 @@ const router = createBrowserRouter([ { path: 'register', element: - }, + } + ] + }, + { + path: '/', + element: , + children: [ { index: true, element: - }, - { - // Placeholder dashboard route - path: 'dashboard', - element:
Dashboard (Protected)
} ] } diff --git a/Lb-web/src/pages/Home.tsx b/Lb-web/src/pages/Home.tsx index 15364d5..4b4844a 100644 --- a/Lb-web/src/pages/Home.tsx +++ b/Lb-web/src/pages/Home.tsx @@ -1,7 +1,16 @@ import { Button } from "@/components/ui/button"; -import { User, Settings, Plus } from "lucide-react"; +import { User, Settings, Plus, LogOut } from "lucide-react"; +import { useState } from "react"; +import { useAuth } from "@/features/Login/context/AuthContext"; export default function Home() { + const { logout } = useAuth(); + const [isProfileOpen, setIsProfileOpen] = useState(false); + + const handleLogout = async () => { + await logout(); + }; + return (
{/* Subtle Grid & Gradient Background */} @@ -12,16 +21,43 @@ export default function Home() {
{/* Navbar */} -
+
Localbase Logo

Localbase

- +
+ + + {isProfileOpen && ( + <> + {/* Transparent backdrop to close on click outside */} +
setIsProfileOpen(false)}>
+ +
+
+ +
+
+ + )} +