diff --git a/Internal/adapters/repository/authRepo.go b/Internal/adapters/repository/authRepo.go
index d4c80fb..cbe6488 100644
--- a/Internal/adapters/repository/authRepo.go
+++ b/Internal/adapters/repository/authRepo.go
@@ -24,3 +24,29 @@ func (ar *AuthRepo) LoginRepo(email string) (*domain.User, error) {
}
return &user, nil
}
+
+func (ar *AuthRepo) CreateInvite(invite *domain.Invitation) error {
+ if err := ar.DB.Gorm.Create(invite).Error; err != nil {
+ return err
+ }
+ return nil
+}
+
+func (ar *AuthRepo) FindInviteByToken(token string) (*domain.Invitation, error) {
+ var invite domain.Invitation
+ if err := ar.DB.Gorm.Where("token = ?", token).First(&invite).Error; err != nil {
+ return nil, err
+ }
+ return &invite, nil
+}
+
+func (ar *AuthRepo) MarkInviteUsed(token string) error {
+ var invite domain.Invitation
+ if err := ar.DB.Gorm.Where("token = ?", token).First(&invite).Error; err != nil {
+ return err
+ }
+ if err := ar.DB.Gorm.Model(&invite).Update("used", true).Error; err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/Internal/adapters/repository/postgres.go b/Internal/adapters/repository/postgres.go
index bc02520..e1974f5 100644
--- a/Internal/adapters/repository/postgres.go
+++ b/Internal/adapters/repository/postgres.go
@@ -40,6 +40,7 @@ func ConnectDB() (*DBContainer, error) {
func (db *DBContainer) Migrate() error {
err := db.Gorm.AutoMigrate(
&domain.User{},
+ &domain.Invitation{},
)
if err != nil {
log.Fatalf("Failed to migrate users: %v", err)
diff --git a/Internal/api/Routes/routev1.go b/Internal/api/Routes/routev1.go
index db257b4..15e0161 100644
--- a/Internal/api/Routes/routev1.go
+++ b/Internal/api/Routes/routev1.go
@@ -40,6 +40,10 @@ func SetupRouterV1(r *gin.Engine, deps Deps) {
{
user.GET("/me", deps.User.GetUserHandler)
}
+ admin := protected.Group("/admin")
+ {
+ admin.POST("/invite", deps.Auth.CreateInviteHandler)
+ }
}
}
}
diff --git a/Internal/api/handlers/authHandler.go b/Internal/api/handlers/authHandler.go
index c6a3932..75506bc 100644
--- a/Internal/api/handlers/authHandler.go
+++ b/Internal/api/handlers/authHandler.go
@@ -11,6 +11,11 @@ type AuthHandler struct {
Service *services.AuthService
}
+type InviteInput struct {
+ Email string `json:"email"`
+ Role string `json:"role"`
+}
+
func NewAuthHandler(service *services.AuthService) *AuthHandler {
return &AuthHandler{Service: service}
}
@@ -69,3 +74,29 @@ func (ah *AuthHandler) LogoutHandler(c *gin.Context) {
"data": nil,
})
}
+
+func (ah *AuthHandler) CreateInviteHandler(c *gin.Context) {
+ var req InviteInput
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(400, gin.H{
+ "message": "Invalid Input",
+ "data": err.Error(),
+ })
+ return
+ }
+ if req.Role == "" {
+ req.Role = "user"
+ }
+ link, err := ah.Service.GenerateInviteService(req.Email, req.Role)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": "Invite service failed",
+ "data": err.Error(),
+ })
+ return
+ }
+ c.JSON(200, gin.H{
+ "message": "Invite service success",
+ "data": link,
+ })
+}
diff --git a/Internal/api/middleware/auth.go b/Internal/api/middleware/auth.go
index d274e69..d9a6038 100644
--- a/Internal/api/middleware/auth.go
+++ b/Internal/api/middleware/auth.go
@@ -73,10 +73,6 @@ func AuthMiddleware(userRepo *repository.UserRepository, cfg *config.Config) gin
c.Set("userRole", user.Role)
c.Next()
- c.JSON(200, gin.H{
- "message": "Success",
- "data": " ",
- })
} else {
c.JSON(401, gin.H{
"message": "Invalid token",
diff --git a/Internal/core/domain/invitationdb.go b/Internal/core/domain/invitationdb.go
new file mode 100644
index 0000000..de3dcec
--- /dev/null
+++ b/Internal/core/domain/invitationdb.go
@@ -0,0 +1,13 @@
+package domain
+
+import "time"
+
+type Invitation struct {
+ ID uint `gorm:"primary_key;AUTO_INCREMENT" json:"id"`
+ Email string `gorm:"size:255; not null" json:"email"`
+ Token string `gorm:"size:255; not null; unique; index" json:"token"`
+ Role string `gorm:"default:'user'; size:255; not null" json:"role"`
+ Used bool `gorm:"default:false" json:"used"`
+ CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
+ ExpiresAt time.Time `json:"expires_at"`
+}
diff --git a/Internal/core/services/authService.go b/Internal/core/services/authService.go
index 848445e..8c7145f 100644
--- a/Internal/core/services/authService.go
+++ b/Internal/core/services/authService.go
@@ -1,17 +1,20 @@
package services
import (
+ "errors"
"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"
+ "github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
type AuthService struct {
- Repo *repository.AuthRepo
+ AuthRepo *repository.AuthRepo
+ SysRepo *repository.SystemRepo
}
type RegisterInput struct {
@@ -19,6 +22,7 @@ type RegisterInput struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Role string `json:"role" binding:"required"`
+ Token string `json:"token"`
}
type LoginInput struct {
@@ -26,28 +30,64 @@ type LoginInput struct {
Password string `json:"password" binding:"required,min=6"`
}
-func NewAuthService(repo *repository.AuthRepo) *AuthService {
- return &AuthService{Repo: repo}
+func NewAuthService(auth *repository.AuthRepo, sys *repository.SystemRepo) *AuthService {
+ return &AuthService{AuthRepo: auth, SysRepo: sys}
}
func (as *AuthService) RegisterService(input *RegisterInput) error {
- user := domain.User{
- Username: input.Username,
- Email: input.Email,
- Password: input.Password,
- Role: input.Role,
- }
- err := as.Repo.RegisterRepo(&user)
+ isInitialized, err := as.SysRepo.IsAppInitialized()
if err != nil {
return err
}
+
+ if !isInitialized {
+ user := domain.User{
+ Username: input.Username,
+ Email: input.Email,
+ Password: input.Password,
+ Role: "admin",
+ }
+
+ if err := as.AuthRepo.RegisterRepo(&user); err != nil {
+ return err
+ }
+
+ } else {
+ if input.Token == "" {
+ return errors.New("token is required")
+ }
+
+ invite, err := as.VerifyInviteService(input.Token)
+ if err != nil {
+ return err
+ }
+
+ if invite.Email == "" && invite.Email != input.Email {
+ return errors.New("email does not match")
+ }
+
+ user := domain.User{
+ Username: input.Username,
+ Email: invite.Email,
+ Password: input.Password,
+ Role: invite.Role,
+ }
+
+ if err := as.AuthRepo.MarkInviteUsed(input.Token); err != nil {
+ return err
+ }
+
+ if err := as.AuthRepo.RegisterRepo(&user); err != nil {
+ return err
+ }
+ }
return nil
}
func (as *AuthService) LoginService(input *LoginInput) (string, error) {
cfg := config.LoadConfig()
- user, err := as.Repo.LoginRepo(input.Email)
+ user, err := as.AuthRepo.LoginRepo(input.Email)
if err != nil {
return "", err
}
@@ -64,3 +104,34 @@ func (as *AuthService) LoginService(input *LoginInput) (string, error) {
}
return tokenString, nil
}
+
+func (as *AuthService) GenerateInviteService(email string, role string) (string, error) {
+ token := uuid.NewString()
+ expiresAt := time.Now().Add(time.Hour * 24)
+ invite := domain.Invitation{
+ Email: email,
+ Token: token,
+ Role: role,
+ Used: false,
+ ExpiresAt: expiresAt,
+ }
+ if err := as.AuthRepo.CreateInvite(&invite); err != nil {
+ return "", err
+ }
+ linkInvite := "http://localhost:5173/registerinvite?token=" + token //Change link if different
+ return linkInvite, nil
+}
+
+func (as *AuthService) VerifyInviteService(token string) (*domain.Invitation, error) {
+ invite, err := as.AuthRepo.FindInviteByToken(token)
+ if err != nil {
+ return nil, err
+ }
+ if invite.Used == true {
+ return nil, errors.New("invite already used")
+ }
+ if time.Now().After(invite.ExpiresAt) {
+ return nil, errors.New("invite expired")
+ }
+ return invite, nil
+}
diff --git a/Lb-web/src/features/Login/components/AuthWrappers.tsx b/Lb-web/src/features/Login/components/AuthWrappers.tsx
index cca6571..a2a2504 100644
--- a/Lb-web/src/features/Login/components/AuthWrappers.tsx
+++ b/Lb-web/src/features/Login/components/AuthWrappers.tsx
@@ -5,7 +5,7 @@ export const ProtectedRoute = () => {
const { isAuthenticated, loading } = useAuth();
if (loading) {
- return
Loading...
;
+ return Loading...
;
}
if (!isAuthenticated) {
@@ -19,7 +19,7 @@ export const PublicRoute = () => {
const { isAuthenticated, loading } = useAuth();
if (loading) {
- return Loading...
;
+ return Loading...
;
}
if (isAuthenticated) {
@@ -28,3 +28,17 @@ export const PublicRoute = () => {
return ;
};
+
+export const AdminRoute = () => {
+ const { role, loading } = useAuth();
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ if (role !== 'admin') {
+ return ;
+ }
+
+ return ;
+};
diff --git a/Lb-web/src/features/Login/context/AuthContext.tsx b/Lb-web/src/features/Login/context/AuthContext.tsx
index 7c2e2dc..f083f77 100644
--- a/Lb-web/src/features/Login/context/AuthContext.tsx
+++ b/Lb-web/src/features/Login/context/AuthContext.tsx
@@ -3,6 +3,7 @@ import api from "@/services/api";
interface AuthContextType {
isAuthenticated: boolean | null;
+ role: string | null;
loading: boolean;
login: () => void; // call this after successful login to update state
logout: () => Promise;
@@ -12,14 +13,17 @@ const AuthContext = createContext(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(null);
+ const [role, setRole] = useState(null);
const [loading, setLoading] = useState(true);
const checkAuth = async () => {
try {
- await api.get('/protected/user/me');
+ const response = await api.get('/protected/user/me');
setIsAuthenticated(true);
+ setRole(response.data.data.role);
} catch {
setIsAuthenticated(false);
+ setRole(null);
} finally {
setLoading(false);
}
@@ -31,6 +35,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const login = () => {
setIsAuthenticated(true);
+ checkAuth(); // Re-fetch user data to get role
};
const logout = async () => {
@@ -40,11 +45,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
console.error("Logout failed", error);
} finally {
setIsAuthenticated(false);
+ setRole(null);
}
};
return (
-
+
{children}
);
diff --git a/Lb-web/src/features/Login/hooks/useLogin.ts b/Lb-web/src/features/Login/hooks/useLogin.ts
index 70192eb..95bb01e 100644
--- a/Lb-web/src/features/Login/hooks/useLogin.ts
+++ b/Lb-web/src/features/Login/hooks/useLogin.ts
@@ -1,6 +1,7 @@
import { useState } from 'react';
-import api from '@/services/api';
+
import { useAuth } from '../context/AuthContext';
+import { authService } from '../services/authService';
export const useLogin = () => {
const [email, setEmail] = useState('');
@@ -15,7 +16,7 @@ export const useLogin = () => {
setError(null);
try {
- await api.post('/auth/Login', { email, password });
+ await authService.login({ email, password });
// Redirect or update global auth state here
// For now, assuming successful cookie set by backend
login(); // Update auth state, wrapper will handle redirect
diff --git a/Lb-web/src/features/Login/hooks/useRegister.ts b/Lb-web/src/features/Login/hooks/useRegister.ts
index cf3d13e..8ebceec 100644
--- a/Lb-web/src/features/Login/hooks/useRegister.ts
+++ b/Lb-web/src/features/Login/hooks/useRegister.ts
@@ -10,6 +10,8 @@ export const useRegister = () => {
// 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 [token, setToken] = useState(null);
+
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const navigate = useNavigate();
@@ -24,7 +26,8 @@ export const useRegister = () => {
username,
email,
password,
- role
+ role,
+ token
});
console.log("Registration successful");
// After successful registration, usually redirect to login or auto-login.
@@ -51,6 +54,8 @@ export const useRegister = () => {
setPassword,
role,
setRole,
+ token,
+ setToken,
loading,
error,
handleRegister,
diff --git a/Lb-web/src/features/Login/register.tsx b/Lb-web/src/features/Login/register.tsx
index b692fce..eb0b6e4 100644
--- a/Lb-web/src/features/Login/register.tsx
+++ b/Lb-web/src/features/Login/register.tsx
@@ -9,6 +9,9 @@ import {
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import { useNavigate, useSearchParams } from "react-router";
+import { useEffect, useState } from "react";
+import { systemService } from "./services/systemService";
import { useRegister } from "./hooks/useRegister";
export default function Register() {
@@ -17,9 +20,48 @@ export default function Register() {
email, setEmail,
password, setPassword,
role, setRole,
- loading, error, handleRegister
+ loading, error, handleRegister, setToken
} = useRegister();
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
+ const [checking, setChecking] = useState(true);
+
+ useEffect(() => {
+ const checkAccess = async () => {
+ const token = searchParams.get("token");
+ if (token) {
+ // If token exists, we assume it's a valid invite (or backend will reject on submit)
+ // Just allow access to the page
+ setToken(token);
+ setChecking(false);
+ return;
+ }
+
+ try {
+ // No token, check if system is already initialized
+ const isInit = await systemService.checkInitStatus();
+ if (isInit) {
+ // System initialized and no token -> unauthorized
+ navigate("/login");
+ } else {
+ // System not initialized -> first user setup -> allow
+ setChecking(false);
+ }
+ } catch (err) {
+ console.error("Failed to check system status", err);
+ // Fail safe? Maybe redirect to login
+ navigate("/login");
+ }
+ };
+
+ checkAccess();
+ }, [navigate, searchParams]);
+
+ if (checking) {
+ return Checking access...
;
+ }
+
return (
{/* Dynamic Background Elements */}
diff --git a/Lb-web/src/features/Login/services/authService.ts b/Lb-web/src/features/Login/services/authService.ts
new file mode 100644
index 0000000..75ba87e
--- /dev/null
+++ b/Lb-web/src/features/Login/services/authService.ts
@@ -0,0 +1,13 @@
+import api from '@/services/api';
+
+export interface LoginRequest {
+ email: string;
+ password?: string;
+ // Add other fields if necessary
+}
+
+export const authService = {
+ login: async (data: LoginRequest) => {
+ return api.post('/auth/Login', data);
+ },
+};
diff --git a/Lb-web/src/features/Login/services/systemService.ts b/Lb-web/src/features/Login/services/systemService.ts
new file mode 100644
index 0000000..8a247ec
--- /dev/null
+++ b/Lb-web/src/features/Login/services/systemService.ts
@@ -0,0 +1,8 @@
+import api from '@/services/api';
+
+export const systemService = {
+ checkInitStatus: async (): Promise
=> {
+ const response = await api.get<{ data: boolean }>('/system/status');
+ return response.data.data;
+ }
+};
diff --git a/Lb-web/src/features/admin-setting/components/SettingsSidebar.tsx b/Lb-web/src/features/admin-setting/components/SettingsSidebar.tsx
index da4e101..83c7d8a 100644
--- a/Lb-web/src/features/admin-setting/components/SettingsSidebar.tsx
+++ b/Lb-web/src/features/admin-setting/components/SettingsSidebar.tsx
@@ -1,47 +1,75 @@
-import { Users } from "lucide-react";
+import { Users, Palette } from "lucide-react";
import { Link, useLocation } from "react-router";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
+import { useAuth } from "@/features/Login/context/AuthContext";
-const sidebarItems = [
+const generalItems = [
+ {
+ title: "Appearance",
+ icon: Palette,
+ href: "/settings/appearance",
+ variant: "ghost"
+ }
+];
+
+const adminItems = [
{
title: "User Management",
icon: Users,
href: "/settings/users",
variant: "ghost"
- },
- // Add more items here in the future
+ }
];
export function SettingsSidebar() {
const location = useLocation();
+ const { role } = useAuth();
+
+ const NavItem = ({ item }: { item: any }) => (
+
+ );
return (