diff --git a/main-plan.md b/main-plan.md
index 37300a0..364fab6 100644
--- a/main-plan.md
+++ b/main-plan.md
@@ -23,7 +23,7 @@ This document provides a complete implementation plan for a 1:1 functional and v
### Backend
- **Runtime**: Node.js 20 LTS
- **Framework**: Express.js with TypeScript
-- **Authentication**: Passport.js (Google OAuth, Microsoft OAuth, local email/password)
+- **Authentication**: Simple admin user + local user management (OAuth deferred to later phases)
- **Session**: express-session with MySQL session store
- **Validation**: Zod (shared with frontend)
- **File Upload**: Multer (for video/image uploads)
@@ -83,7 +83,9 @@ turkshop/
│ │ │ │ ├── Help.tsx
│ │ │ │ ├── Community.tsx
│ │ │ │ ├── Comments.tsx
-│ │ │ │ └── BugReport.tsx
+│ │ │ │ ├── BugReport.tsx
+│ │ │ │ └── admin/
+│ │ │ │ └── UserManagement.tsx
│ │ │ ├── stores/ # Zustand stores
│ │ │ │ ├── authStore.ts
│ │ │ │ ├── sentenceStore.ts
@@ -107,6 +109,7 @@ turkshop/
│ ├── src/
│ │ ├── routes/ # API route handlers
│ │ │ ├── auth.ts
+│ │ │ ├── admin.ts
│ │ │ ├── terms.ts
│ │ │ ├── documents.ts
│ │ │ ├── sentences.ts
@@ -115,6 +118,7 @@ turkshop/
│ │ │ └── index.ts
│ │ ├── controllers/ # Business logic
│ │ │ ├── authController.ts
+│ │ │ ├── adminController.ts
│ │ │ ├── termController.ts
│ │ │ ├── documentController.ts
│ │ │ └── sentenceController.ts
@@ -129,7 +133,7 @@ turkshop/
│ │ │ └── playlistService.ts
│ │ ├── lib/ # Utilities
│ │ │ ├── prisma.ts # Prisma client
-│ │ │ └── passport.ts # Passport config
+│ │ │ └── auth.ts # Auth utilities (bcrypt, session)
│ │ ├── types/ # TypeScript types
│ │ │ └── index.ts
│ │ ├── config/ # Configuration
@@ -183,9 +187,9 @@ model User {
id String @id @default(uuid())
email String @unique
displayName String? @map("display_name")
- passwordHash String? @map("password_hash")
- authProvider String @default("local") @map("auth_provider") // local, google, microsoft
- providerId String? @map("provider_id")
+ passwordHash String @map("password_hash")
+ role UserRole @default(USER)
+ isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -196,6 +200,11 @@ model User {
@@map("users")
}
+enum UserRole {
+ ADMIN
+ USER
+}
+
// ============================================
// DICTIONARY / TERMS
// ============================================
@@ -408,16 +417,22 @@ enum BugStatus {
| Method | Endpoint | Description | Auth Required |
|--------|----------|-------------|---------------|
-| POST | `/auth/register` | Register new user with email/password | No |
| POST | `/auth/login` | Login with email/password | No |
| POST | `/auth/logout` | Logout current user | Yes |
| GET | `/auth/me` | Get current user info | Yes |
-| GET | `/auth/google` | Initiate Google OAuth flow | No |
-| GET | `/auth/google/callback` | Google OAuth callback | No |
-| GET | `/auth/microsoft` | Initiate Microsoft OAuth flow | No |
-| GET | `/auth/microsoft/callback` | Microsoft OAuth callback | No |
-### 4.2 Dictionary/Terms Routes (`/api/terms`)
+### 4.2 Admin User Management Routes (`/api/admin/users`)
+
+| Method | Endpoint | Description | Auth Required |
+|--------|----------|-------------|---------------|
+| GET | `/admin/users` | List all users | Yes (Admin) |
+| POST | `/admin/users` | Create new user | Yes (Admin) |
+| PUT | `/admin/users/:id` | Update user | Yes (Admin) |
+| DELETE | `/admin/users/:id` | Delete user | Yes (Admin) |
+| PUT | `/admin/users/:id/password` | Reset user password | Yes (Admin) |
+| PUT | `/admin/users/:id/activate` | Activate/deactivate user | Yes (Admin) |
+
+### 4.3 Dictionary/Terms Routes (`/api/terms`)
| Method | Endpoint | Description | Auth Required |
|--------|----------|-------------|---------------|
@@ -438,7 +453,7 @@ enum BugStatus {
- `sortBy` (string): Sort field (default: wordText)
- `sortOrder` (asc|desc): Sort direction (default: asc)
-### 4.3 Document Routes (`/api/documents`)
+### 4.4 Document Routes (`/api/documents`)
| Method | Endpoint | Description | Auth Required |
|--------|----------|-------------|---------------|
@@ -450,7 +465,7 @@ enum BugStatus {
| PUT | `/documents/:id/content` | Update document content (pages/sentences) | Yes |
| POST | `/documents/:id/share` | Generate share link | Yes |
-### 4.4 Sentence Routes (`/api/sentences`)
+### 4.5 Sentence Routes (`/api/sentences`)
| Method | Endpoint | Description | Auth Required |
|--------|----------|-------------|---------------|
@@ -463,7 +478,7 @@ enum BugStatus {
| DELETE | `/sentences/:id/tokens/:tokenId` | Remove token | Yes |
| PUT | `/sentences/:id/reorder` | Reorder tokens | Yes |
-### 4.5 Playlist Routes (`/api/playlists`)
+### 4.6 Playlist Routes (`/api/playlists`)
| Method | Endpoint | Description | Auth Required |
|--------|----------|-------------|---------------|
@@ -494,7 +509,7 @@ enum BugStatus {
}
```
-### 4.6 Community Routes (`/api/community`)
+### 4.7 Community Routes (`/api/community`)
| Method | Endpoint | Description | Auth Required |
|--------|----------|-------------|---------------|
@@ -502,7 +517,7 @@ enum BugStatus {
| POST | `/community/comments` | Create comment | Yes |
| DELETE | `/community/comments/:id` | Delete own comment | Yes |
-### 4.7 Bug Report Routes (`/api/bugs`)
+### 4.8 Bug Report Routes (`/api/bugs`)
| Method | Endpoint | Description | Auth Required |
|--------|----------|-------------|---------------|
@@ -511,14 +526,14 @@ enum BugStatus {
| POST | `/bugs` | Submit bug report | Yes |
| PUT | `/bugs/:id` | Update bug status (admin) | Yes (Admin) |
-### 4.8 Upload Routes (`/api/uploads`)
+### 4.9 Upload Routes (`/api/uploads`)
| Method | Endpoint | Description | Auth Required |
|--------|----------|-------------|---------------|
| POST | `/uploads/video` | Upload video file | Yes (Admin) |
| POST | `/uploads/icon` | Upload icon/image | Yes (Admin) |
-### 4.9 Static File Serving
+### 4.10 Static File Serving
| Path | Description |
|------|-------------|
@@ -653,6 +668,7 @@ Components to install:
- Zajednica (Community)
- Komentari (Comments)
- Prijavi grešku (Bug Report)
+- Admin Panel (Admin only - User Management)
**Dictionary Page:**
- Riječ (Word) - search input label
@@ -674,14 +690,26 @@ Components to install:
**Auth:**
- Prijavi se (Sign In)
- Odjavi se (Sign Out)
-- Registriraj se (Sign Up)
- Email adresa (Email Address)
- Lozinka (Password)
+**Admin Panel:**
+- Korisnici (Users)
+- Dodaj korisnika (Add User)
+- Uredi korisnika (Edit User)
+- Obriši korisnika (Delete User)
+- Resetiraj lozinku (Reset Password)
+- Aktiviraj/Deaktiviraj (Activate/Deactivate)
+- Uloga (Role)
+- Aktivan (Active)
+
---
## 6. Implementation Milestones
+**Note on Authentication Strategy:**
+For the initial phases, we will implement a simple admin user with local user management. OAuth integration (Google, Microsoft) will be deferred to later phases (Phase 8+) to focus on core functionality first. The admin user will be able to create and manage local users through an admin panel.
+
### Phase 0: Project Setup (Week 1)
**Goal:** Initialize project structure and development environment
@@ -704,34 +732,36 @@ Components to install:
---
### Phase 1: Core Infrastructure (Week 2)
-**Goal:** Build authentication and basic layout
+**Goal:** Build basic authentication and layout with admin user
**Backend Tasks:**
-1. Implement Prisma schema (User model)
+1. Implement Prisma schema (User model with role and isActive fields)
2. Run initial migration
-3. Set up Passport.js with local strategy
-4. Implement session management
-5. Create auth routes (register, login, logout, me)
-6. Set up Google OAuth
-7. Set up Microsoft OAuth
-8. Create auth middleware
-9. Set up CORS and security headers
+3. Create seed script with default admin user
+4. Implement simple session-based authentication
+5. Create auth routes (login, logout, me)
+6. Create auth middleware (isAuthenticated, isAdmin)
+7. Set up CORS and security headers
+8. Create admin user management routes (CRUD)
**Frontend Tasks:**
1. Create layout components (Sidebar, Header, Layout)
2. Set up React Router with routes
-3. Create auth pages (Login, Register)
+3. Create login page (simple email/password)
4. Implement auth store (Zustand)
5. Create auth API client
6. Implement protected routes
-7. Add OAuth buttons
+7. Create admin panel page for user management
+8. Build user list, create, edit, delete components
**Deliverables:**
-- ✅ Users can register and login
-- ✅ OAuth works (Google + Microsoft)
+- ✅ Admin can login with default credentials
- ✅ Session persists across page reloads
- ✅ Sidebar navigation works
- ✅ Protected routes redirect to login
+- ✅ Admin can create/edit/delete local users
+- ✅ Admin can reset user passwords
+- ✅ Admin can activate/deactivate users
---
@@ -918,6 +948,31 @@ Components to install:
---
+### Phase 8: OAuth Integration (Future Phase)
+**Goal:** Add OAuth authentication providers (deferred from Phase 1)
+
+**Backend Tasks:**
+1. Set up Passport.js with Google OAuth strategy
+2. Set up Passport.js with Microsoft OAuth strategy
+3. Update User model to support authProvider and providerId fields
+4. Create OAuth callback routes
+5. Implement account linking (OAuth to existing local accounts)
+6. Update auth middleware to support OAuth sessions
+
+**Frontend Tasks:**
+1. Add OAuth buttons to login page
+2. Create OAuth callback handling
+3. Add account linking UI
+4. Update user profile to show auth provider
+
+**Deliverables:**
+- ✅ Users can sign in with Google
+- ✅ Users can sign in with Microsoft
+- ✅ OAuth accounts can be linked to existing local accounts
+- ✅ Session management works with OAuth
+
+---
+
## 7. Environment Configuration
### Backend `.env` (packages/backend/.env)
@@ -934,15 +989,9 @@ DATABASE_URL="mysql://user:password@localhost:3306/znakovni"
# Session
SESSION_SECRET=your-super-secret-session-key-change-in-production
-# OAuth - Google
-GOOGLE_CLIENT_ID=your-google-client-id
-GOOGLE_CLIENT_SECRET=your-google-client-secret
-GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback
-
-# OAuth - Microsoft
-MICROSOFT_CLIENT_ID=your-microsoft-client-id
-MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
-MICROSOFT_CALLBACK_URL=http://localhost:3000/api/auth/microsoft/callback
+# Default Admin User (created on first run)
+ADMIN_EMAIL=admin@znakovni.hr
+ADMIN_PASSWORD=change-this-password-immediately
# File Upload
UPLOAD_DIR=./uploads
@@ -1290,7 +1339,11 @@ npx shadcn-ui@latest add card
- [ ] Sentences can be saved to documents
- [ ] Documents can be loaded from cloud
- [ ] Video sentence player works with sequential playback
-- [ ] Authentication works (email + Google + Microsoft)
+- [ ] Admin can login with credentials
+- [ ] Admin can create/edit/delete local users
+- [ ] Admin can reset user passwords
+- [ ] Admin can activate/deactivate users
+- [ ] Regular users can login with their credentials
- [ ] Cloud document management works
- [ ] Comments and bug reports can be submitted
@@ -1321,12 +1374,12 @@ npx shadcn-ui@latest add card
## 13. Next Steps
1. **Review and approve this plan**
-2. **Set up OAuth credentials** (Google Cloud Console, Microsoft Azure)
-3. **Prepare MySQL database** (local or cloud)
-4. **Begin Phase 0: Project Setup**
-5. **Iterate through phases sequentially**
-6. **Test continuously during development**
-7. **Deploy to production server**
+2. **Prepare MySQL database** (local or cloud)
+3. **Begin Phase 0: Project Setup**
+4. **Iterate through phases sequentially**
+5. **Test continuously during development**
+6. **Deploy to production server**
+7. **Phase 8+ (Future): Set up OAuth credentials** (Google Cloud Console, Microsoft Azure) when ready to add OAuth
---
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 34bab88..4662ebd 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -14,6 +14,9 @@
"prisma:studio": "prisma studio",
"prisma:seed": "tsx prisma/seed.ts"
},
+ "prisma": {
+ "seed": "tsx prisma/seed.ts"
+ },
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
diff --git a/packages/backend/prisma/schema.prisma b/packages/backend/prisma/schema.prisma
index 2bffe93..ee6b8ae 100644
--- a/packages/backend/prisma/schema.prisma
+++ b/packages/backend/prisma/schema.prisma
@@ -8,17 +8,25 @@ generator client {
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
+ shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
}
// ============================================
// AUTHENTICATION & USERS
// ============================================
+enum UserRole {
+ ADMIN
+ USER
+}
+
model User {
id String @id @default(uuid())
email String @unique
displayName String? @map("display_name")
passwordHash String? @map("password_hash")
+ role UserRole @default(USER)
+ isActive Boolean @default(true) @map("is_active")
authProvider String @default("local") @map("auth_provider") // local, google, microsoft
providerId String? @map("provider_id")
createdAt DateTime @default(now()) @map("created_at")
diff --git a/packages/backend/prisma/seed.ts b/packages/backend/prisma/seed.ts
index 9b2033a..de4f8dd 100644
--- a/packages/backend/prisma/seed.ts
+++ b/packages/backend/prisma/seed.ts
@@ -1,20 +1,48 @@
import { PrismaClient } from '@prisma/client';
+import bcrypt from 'bcrypt';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Starting database seed...');
- // Create a sample user
- const user = await prisma.user.create({
- data: {
- email: 'demo@znakovni.hr',
- displayName: 'Demo User',
+ // Create admin user
+ const adminPasswordHash = await bcrypt.hash('admin123', 10);
+ const admin = await prisma.user.upsert({
+ where: { email: 'admin@znakovni.hr' },
+ update: {},
+ create: {
+ email: 'admin@znakovni.hr',
+ displayName: 'Administrator',
+ passwordHash: adminPasswordHash,
+ role: 'ADMIN',
+ isActive: true,
authProvider: 'local',
},
});
- console.log('✅ Created demo user:', user.email);
+ console.log('✅ Created admin user:', admin.email);
+ console.log(' Email: admin@znakovni.hr');
+ console.log(' Password: admin123');
+
+ // Create a demo regular user
+ const demoPasswordHash = await bcrypt.hash('demo123', 10);
+ const demoUser = await prisma.user.upsert({
+ where: { email: 'demo@znakovni.hr' },
+ update: {},
+ create: {
+ email: 'demo@znakovni.hr',
+ displayName: 'Demo User',
+ passwordHash: demoPasswordHash,
+ role: 'USER',
+ isActive: true,
+ authProvider: 'local',
+ },
+ });
+
+ console.log('✅ Created demo user:', demoUser.email);
+ console.log(' Email: demo@znakovni.hr');
+ console.log(' Password: demo123');
// Add sample terms here in future phases
console.log('✅ Seed completed successfully!');
diff --git a/packages/backend/src/lib/passport.ts b/packages/backend/src/lib/passport.ts
new file mode 100644
index 0000000..0d02f6a
--- /dev/null
+++ b/packages/backend/src/lib/passport.ts
@@ -0,0 +1,74 @@
+import passport from 'passport';
+import { Strategy as LocalStrategy } from 'passport-local';
+import bcrypt from 'bcrypt';
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+// Configure local strategy
+passport.use(
+ new LocalStrategy(
+ {
+ usernameField: 'email',
+ passwordField: 'password',
+ },
+ async (email, password, done) => {
+ try {
+ // Find user by email
+ const user = await prisma.user.findUnique({
+ where: { email },
+ });
+
+ if (!user) {
+ return done(null, false, { message: 'Invalid email or password' });
+ }
+
+ // Check if user is active
+ if (!user.isActive) {
+ return done(null, false, { message: 'Account is deactivated' });
+ }
+
+ // Check if user has a password (local auth)
+ if (!user.passwordHash) {
+ return done(null, false, { message: 'Please use OAuth to login' });
+ }
+
+ // Verify password
+ const isValidPassword = await bcrypt.compare(password, user.passwordHash);
+ if (!isValidPassword) {
+ return done(null, false, { message: 'Invalid email or password' });
+ }
+
+ // Success
+ return done(null, user);
+ } catch (error) {
+ return done(error);
+ }
+ }
+ )
+);
+
+// Serialize user to session
+passport.serializeUser((user: any, done) => {
+ done(null, user.id);
+});
+
+// Deserialize user from session
+passport.deserializeUser(async (id: string, done) => {
+ try {
+ const user = await prisma.user.findUnique({
+ where: { id },
+ });
+
+ if (!user || !user.isActive) {
+ return done(null, false);
+ }
+
+ done(null, user);
+ } catch (error) {
+ done(error);
+ }
+});
+
+export default passport;
+
diff --git a/packages/backend/src/lib/prisma.ts b/packages/backend/src/lib/prisma.ts
new file mode 100644
index 0000000..b6dfa0c
--- /dev/null
+++ b/packages/backend/src/lib/prisma.ts
@@ -0,0 +1,12 @@
+import { PrismaClient } from '@prisma/client';
+
+const globalForPrisma = global as unknown as { prisma: PrismaClient };
+
+export const prisma =
+ globalForPrisma.prisma ||
+ new PrismaClient({
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
+ });
+
+if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
+
diff --git a/packages/backend/src/middleware/auth.ts b/packages/backend/src/middleware/auth.ts
new file mode 100644
index 0000000..b7567e6
--- /dev/null
+++ b/packages/backend/src/middleware/auth.ts
@@ -0,0 +1,45 @@
+import { Request, Response, NextFunction } from 'express';
+
+// Extend Express Request type to include user
+declare global {
+ namespace Express {
+ interface User {
+ id: string;
+ email: string;
+ displayName: string | null;
+ role: 'ADMIN' | 'USER';
+ isActive: boolean;
+ }
+ }
+}
+
+/**
+ * Middleware to check if user is authenticated
+ */
+export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
+ if (req.isAuthenticated()) {
+ return next();
+ }
+ res.status(401).json({ error: 'Unauthorized', message: 'Please login to continue' });
+};
+
+/**
+ * Middleware to check if user is an admin
+ */
+export const isAdmin = (req: Request, res: Response, next: NextFunction) => {
+ if (req.isAuthenticated() && req.user?.role === 'ADMIN') {
+ return next();
+ }
+ res.status(403).json({ error: 'Forbidden', message: 'Admin access required' });
+};
+
+/**
+ * Middleware to check if user is active
+ */
+export const isActive = (req: Request, res: Response, next: NextFunction) => {
+ if (req.isAuthenticated() && req.user?.isActive) {
+ return next();
+ }
+ res.status(403).json({ error: 'Forbidden', message: 'Account is deactivated' });
+};
+
diff --git a/packages/backend/src/routes/auth.ts b/packages/backend/src/routes/auth.ts
new file mode 100644
index 0000000..8787727
--- /dev/null
+++ b/packages/backend/src/routes/auth.ts
@@ -0,0 +1,82 @@
+import { Router, Request, Response, NextFunction } from 'express';
+import passport from '../lib/passport.js';
+import { isAuthenticated } from '../middleware/auth.js';
+
+const router = Router();
+
+/**
+ * POST /api/auth/login
+ * Login with email and password
+ */
+router.post('/login', (req: Request, res: Response, next: NextFunction) => {
+ passport.authenticate('local', (err: any, user: any, info: any) => {
+ if (err) {
+ return res.status(500).json({ error: 'Internal server error', message: err.message });
+ }
+
+ if (!user) {
+ return res.status(401).json({
+ error: 'Authentication failed',
+ message: info?.message || 'Invalid credentials'
+ });
+ }
+
+ req.logIn(user, (err) => {
+ if (err) {
+ return res.status(500).json({ error: 'Login failed', message: err.message });
+ }
+
+ // Return user data without sensitive fields
+ const { passwordHash, ...userWithoutPassword } = user;
+ res.json({
+ message: 'Login successful',
+ user: userWithoutPassword
+ });
+ });
+ })(req, res, next);
+});
+
+/**
+ * POST /api/auth/logout
+ * Logout current user
+ */
+router.post('/logout', (req: Request, res: Response) => {
+ req.logout((err) => {
+ if (err) {
+ return res.status(500).json({ error: 'Logout failed', message: err.message });
+ }
+ res.json({ message: 'Logout successful' });
+ });
+});
+
+/**
+ * GET /api/auth/me
+ * Get current authenticated user
+ */
+router.get('/me', isAuthenticated, (req: Request, res: Response) => {
+ if (!req.user) {
+ return res.status(401).json({ error: 'Not authenticated' });
+ }
+
+ // Return user data without sensitive fields
+ const user = req.user as any;
+ const { passwordHash, ...userWithoutPassword } = user;
+ res.json({ user: userWithoutPassword });
+});
+
+/**
+ * GET /api/auth/check
+ * Check if user is authenticated (public endpoint)
+ */
+router.get('/check', (req: Request, res: Response) => {
+ if (req.isAuthenticated() && req.user) {
+ const user = req.user as any;
+ const { passwordHash, ...userWithoutPassword } = user;
+ res.json({ authenticated: true, user: userWithoutPassword });
+ } else {
+ res.json({ authenticated: false, user: null });
+ }
+});
+
+export default router;
+
diff --git a/packages/backend/src/routes/users.ts b/packages/backend/src/routes/users.ts
new file mode 100644
index 0000000..b1bae5a
--- /dev/null
+++ b/packages/backend/src/routes/users.ts
@@ -0,0 +1,196 @@
+import { Router, Request, Response } from 'express';
+import bcrypt from 'bcrypt';
+import { prisma } from '../lib/prisma.js';
+import { isAuthenticated, isAdmin } from '../middleware/auth.js';
+
+const router = Router();
+
+// All routes require admin authentication
+router.use(isAuthenticated, isAdmin);
+
+/**
+ * GET /api/users
+ * Get all users (admin only)
+ */
+router.get('/', async (_req: Request, res: Response) => {
+ try {
+ const users = await prisma.user.findMany({
+ select: {
+ id: true,
+ email: true,
+ displayName: true,
+ role: true,
+ isActive: true,
+ authProvider: true,
+ createdAt: true,
+ updatedAt: true,
+ },
+ orderBy: {
+ createdAt: 'desc',
+ },
+ });
+
+ res.json({ users });
+ } catch (error: any) {
+ res.status(500).json({ error: 'Failed to fetch users', message: error.message });
+ }
+});
+
+/**
+ * GET /api/users/:id
+ * Get user by ID (admin only)
+ */
+router.get('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params;
+
+ const user = await prisma.user.findUnique({
+ where: { id },
+ select: {
+ id: true,
+ email: true,
+ displayName: true,
+ role: true,
+ isActive: true,
+ authProvider: true,
+ createdAt: true,
+ updatedAt: true,
+ },
+ });
+
+ if (!user) {
+ return res.status(404).json({ error: 'User not found' });
+ }
+
+ res.json({ user });
+ } catch (error: any) {
+ res.status(500).json({ error: 'Failed to fetch user', message: error.message });
+ }
+});
+
+/**
+ * POST /api/users
+ * Create new user (admin only)
+ */
+router.post('/', async (req: Request, res: Response) => {
+ try {
+ const { email, displayName, password, role, isActive } = req.body;
+
+ // Validate required fields
+ if (!email || !password) {
+ return res.status(400).json({ error: 'Email and password are required' });
+ }
+
+ // Check if user already exists
+ const existingUser = await prisma.user.findUnique({
+ where: { email },
+ });
+
+ if (existingUser) {
+ return res.status(400).json({ error: 'User with this email already exists' });
+ }
+
+ // Hash password
+ const passwordHash = await bcrypt.hash(password, 10);
+
+ // Create user
+ const user = await prisma.user.create({
+ data: {
+ email,
+ displayName: displayName || null,
+ passwordHash,
+ role: role || 'USER',
+ isActive: isActive !== undefined ? isActive : true,
+ authProvider: 'local',
+ },
+ select: {
+ id: true,
+ email: true,
+ displayName: true,
+ role: true,
+ isActive: true,
+ authProvider: true,
+ createdAt: true,
+ updatedAt: true,
+ },
+ });
+
+ res.status(201).json({ message: 'User created successfully', user });
+ } catch (error: any) {
+ res.status(500).json({ error: 'Failed to create user', message: error.message });
+ }
+});
+
+/**
+ * PATCH /api/users/:id
+ * Update user (admin only)
+ */
+router.patch('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params;
+ const { email, displayName, role, isActive, password } = req.body;
+
+ const updateData: any = {};
+
+ if (email !== undefined) updateData.email = email;
+ if (displayName !== undefined) updateData.displayName = displayName;
+ if (role !== undefined) updateData.role = role;
+ if (isActive !== undefined) updateData.isActive = isActive;
+
+ // Hash new password if provided
+ if (password) {
+ updateData.passwordHash = await bcrypt.hash(password, 10);
+ }
+
+ const user = await prisma.user.update({
+ where: { id },
+ data: updateData,
+ select: {
+ id: true,
+ email: true,
+ displayName: true,
+ role: true,
+ isActive: true,
+ authProvider: true,
+ createdAt: true,
+ updatedAt: true,
+ },
+ });
+
+ res.json({ message: 'User updated successfully', user });
+ } catch (error: any) {
+ if (error.code === 'P2025') {
+ return res.status(404).json({ error: 'User not found' });
+ }
+ res.status(500).json({ error: 'Failed to update user', message: error.message });
+ }
+});
+
+/**
+ * DELETE /api/users/:id
+ * Delete user (admin only)
+ */
+router.delete('/:id', async (req: Request, res: Response) => {
+ try {
+ const { id } = req.params;
+
+ // Prevent admin from deleting themselves
+ if (req.user?.id === id) {
+ return res.status(400).json({ error: 'Cannot delete your own account' });
+ }
+
+ await prisma.user.delete({
+ where: { id },
+ });
+
+ res.json({ message: 'User deleted successfully' });
+ } catch (error: any) {
+ if (error.code === 'P2025') {
+ return res.status(404).json({ error: 'User not found' });
+ }
+ res.status(500).json({ error: 'Failed to delete user', message: error.message });
+ }
+});
+
+export default router;
+
diff --git a/packages/backend/src/server.ts b/packages/backend/src/server.ts
index 46b88ad..a02e083 100644
--- a/packages/backend/src/server.ts
+++ b/packages/backend/src/server.ts
@@ -2,6 +2,8 @@ import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
+import session from 'express-session';
+import passport from './lib/passport.js';
dotenv.config();
@@ -17,10 +19,37 @@ app.use(cors({
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
+// Session configuration
+app.use(
+ session({
+ secret: process.env.SESSION_SECRET || 'your-secret-key-change-in-production',
+ resave: false,
+ saveUninitialized: false,
+ cookie: {
+ secure: process.env.NODE_ENV === 'production',
+ httpOnly: true,
+ maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
+ sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
+ },
+ })
+);
+
+// Passport middleware
+app.use(passport.initialize());
+app.use(passport.session());
+
+// Import routes
+import authRoutes from './routes/auth.js';
+import userRoutes from './routes/users.js';
+
+// API routes
+app.use('/api/auth', authRoutes);
+app.use('/api/users', userRoutes);
+
// Health check route
app.get('/api/health', (_req, res) => {
- res.json({
- status: 'ok',
+ res.json({
+ status: 'ok',
message: 'Backend is running!',
timestamp: new Date().toISOString(),
});
@@ -28,15 +57,16 @@ app.get('/api/health', (_req, res) => {
// Root route
app.get('/', (_req, res) => {
- res.json({
+ res.json({
message: 'Znakovni.hr API',
version: '1.0.0',
});
});
// Start server
-app.listen(PORT, () => {
- console.log(`🚀 Server running on http://localhost:${PORT}`);
+const HOST = process.env.HOST || '0.0.0.0';
+app.listen(PORT, HOST, () => {
+ console.log(`🚀 Server running on http://${HOST}:${PORT}`);
console.log(`📝 Environment: ${process.env.NODE_ENV || 'development'}`);
});
diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index 967d3f2..3f91ab5 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -1,11 +1,47 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import { useEffect } from 'react';
import Home from './pages/Home';
+import { Login } from './pages/Login';
+import { Admin } from './pages/Admin';
+import { ProtectedRoute } from './components/ProtectedRoute';
+import { useAuthStore } from './stores/authStore';
function App() {
+ const { checkAuth } = useAuthStore();
+
+ useEffect(() => {
+ checkAuth();
+ }, [checkAuth]);
+
return (
Loading...
+{user.displayName || user.email}
+{user.email}
+Loading users...
+| + User + | ++ Role + | ++ Status + | ++ Created + | ++ Actions + | +
|---|---|---|---|---|
|
+
+
+
+ {user.displayName || user.email}
+
+ {user.email}
+ |
+ + + {user.role} + + | ++ + {user.isActive ? 'Active' : 'Inactive'} + + | ++ {new Date(user.createdAt).toLocaleDateString()} + | +
+
+
+
+
+ |
+
- Hrvatski znakovni jezik - Rječnik i platforma za učenje -
-- Frontend is running! ✅ -
++ Hrvatski znakovni jezik - platforma za učenje i komunikaciju +
+{feature.description}
++ Hrvatski znakovni jezik +
+Demo Credentials:
+Admin: admin@znakovni.hr / admin123
+User: demo@znakovni.hr / demo123
+