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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Internal/adapters/repository/authRepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions Internal/adapters/repository/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions Internal/api/Routes/routev1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
31 changes: 31 additions & 0 deletions Internal/api/handlers/authHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
Expand Down Expand Up @@ -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,
})
}
4 changes: 0 additions & 4 deletions Internal/api/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions Internal/core/domain/invitationdb.go
Original file line number Diff line number Diff line change
@@ -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"`
}
93 changes: 82 additions & 11 deletions Internal/core/services/authService.go
Original file line number Diff line number Diff line change
@@ -1,53 +1,93 @@
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 {
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"`
Token string `json:"token"`
}

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 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
}
Expand All @@ -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
}
18 changes: 16 additions & 2 deletions Lb-web/src/features/Login/components/AuthWrappers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const ProtectedRoute = () => {
const { isAuthenticated, loading } = useAuth();

if (loading) {
return <div className="flex h-screen w-screen items-center justify-center bg-slate-50 text-sky-900">Loading...</div>;
return <div className="flex h-screen w-screen items-center justify-center bg-sky-50 text-sky-900">Loading...</div>;
}

if (!isAuthenticated) {
Expand All @@ -19,7 +19,7 @@ export const PublicRoute = () => {
const { isAuthenticated, loading } = useAuth();

if (loading) {
return <div className="flex h-screen w-screen items-center justify-center bg-slate-50 text-sky-900">Loading...</div>;
return <div className="flex h-screen w-screen items-center justify-center bg-sky-50 text-sky-900">Loading...</div>;
}

if (isAuthenticated) {
Expand All @@ -28,3 +28,17 @@ export const PublicRoute = () => {

return <Outlet />;
};

export const AdminRoute = () => {
const { role, loading } = useAuth();

if (loading) {
return <div className="flex h-screen w-screen items-center justify-center bg-sky-50 text-sky-900">Loading...</div>;
}

if (role !== 'admin') {
return <Navigate to="/settings/appearance" replace />;
}

return <Outlet />;
};
10 changes: 8 additions & 2 deletions Lb-web/src/features/Login/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

interface AuthContextType {
isAuthenticated: boolean | null;
role: string | null;
loading: boolean;
login: () => void; // call this after successful login to update state
logout: () => Promise<void>;
Expand All @@ -12,14 +13,17 @@

export function AuthProvider({ children }: { children: ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const [role, setRole] = useState<string | null>(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);
}
Expand All @@ -31,6 +35,7 @@

const login = () => {
setIsAuthenticated(true);
checkAuth(); // Re-fetch user data to get role
};

const logout = async () => {
Expand All @@ -40,17 +45,18 @@
console.error("Logout failed", error);
} finally {
setIsAuthenticated(false);
setRole(null);
}
};

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

export function useAuth() {

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

View workflow job for this annotation

GitHub Actions / Frontend integrity checks

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
Expand Down
5 changes: 3 additions & 2 deletions Lb-web/src/features/Login/hooks/useLogin.ts
Original file line number Diff line number Diff line change
@@ -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('');
Expand All @@ -15,12 +16,12 @@
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
console.log("Login successful");
} catch (err: any) {

Check failure on line 24 in Lb-web/src/features/Login/hooks/useLogin.ts

View workflow job for this annotation

GitHub Actions / Frontend integrity checks

Unexpected any. Specify a different type
console.error("Login failed", err);
if (err.response && err.response.data && err.response.data.message) {
setError(err.response.data.message);
Expand Down
7 changes: 6 additions & 1 deletion Lb-web/src/features/Login/hooks/useRegister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
// 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<string | null>(null);

const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
Expand All @@ -24,13 +26,14 @@
username,
email,
password,
role
role,
token
});
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) {

Check failure on line 36 in Lb-web/src/features/Login/hooks/useRegister.ts

View workflow job for this annotation

GitHub Actions / Frontend integrity checks

Unexpected any. Specify a different type
console.error("Registration failed", err);
if (err.response && err.response.data && err.response.data.message) {
setError(err.response.data.message);
Expand All @@ -51,6 +54,8 @@
setPassword,
role,
setRole,
token,
setToken,
loading,
error,
handleRegister,
Expand Down
Loading
Loading