Explorar el Código

Add user management and minor fixes

svalavuo hace 1 día
padre
commit
223739e41f

+ 2 - 2
backend/.env.local

@@ -1,9 +1,9 @@
 # Development Database Configuration
-DB_HOST=10.8.10.31
+DB_HOST=192.168.0.101
 DB_PORT=3306
 DB_NAME=inventory_db
 DB_USER=inventory_db
-DB_PASS=mDw(HF]Cub.UM2*7
+DB_PASS=mDwHF]CubUM27
 MYSQL_ROOT_PASSWORD=jotainaivanmuuta
 
 # Port Configuration

+ 21 - 1
backend/api/auth.php

@@ -23,7 +23,20 @@ $request_method = $_SERVER['REQUEST_METHOD'];
 
 switch($request_method) {
     case 'GET':
-        if(isset($_GET['action']) && $_GET['action'] === 'status') {
+        if(isset($_GET['action']) && $_GET['action'] === 'current-user') {
+            // Get current authenticated user
+            require_once __DIR__ . '/../middleware/auth.php';
+            $auth = new AuthMiddleware($db);
+            $current_user = $auth->authenticate();
+            
+            if ($current_user) {
+                http_response_code(200);
+                echo json_encode($current_user);
+            } else {
+                http_response_code(401);
+                echo json_encode(array("message" => "Not authenticated"));
+            }
+        } elseif(isset($_GET['action']) && $_GET['action'] === 'status') {
             // Check if user is logged in
             if(isset($_SESSION['user_id']) && !empty($_SESSION['user_id'])) {
                 // Validate session by checking if user still exists and is active
@@ -132,7 +145,14 @@ switch($request_method) {
                 echo json_encode(array("message" => "Required fields are missing"));
             }
         } elseif($data->action === 'logout') {
+            // Start session if not already started
+            if (session_status() === PHP_SESSION_NONE) {
+                session_start();
+            }
+            
+            // Destroy session
             session_destroy();
+            
             http_response_code(200);
             echo json_encode(array("message" => "Logged out successfully"));
         } else {

+ 73 - 8
backend/api/users.php

@@ -10,21 +10,40 @@ if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
 
 require_once __DIR__ . '/../config/database.php';
 require_once __DIR__ . '/../models/User.php';
+require_once __DIR__ . '/../middleware/auth.php';
+
+// Start session for authentication
+session_start();
 
 $database = new Database();
 $db = $database->getConnection();
 
 $user = new User($db);
+$auth = new AuthMiddleware($db);
+
+// Authenticate user
+$current_user = $auth->authenticate();
+if (!$current_user) {
+    $auth->sendUnauthorizedResponse();
+}
 
 $request_method = $_SERVER['REQUEST_METHOD'];
 
 switch($request_method) {
     case 'GET':
         if(isset($_GET['id'])) {
-            $user->id = $_GET['id'];
+            $user_id = $_GET['id'];
+            
+            // Check authorization: Admin can access any user, users can only access their own profile
+            if (!$auth->canAccess($current_user, null, $user_id)) {
+                $auth->sendForbiddenResponse();
+            }
+            
+            $user->id = $user_id;
             $user->readOne();
             
             if($user->username != null) {
+                // Remove sensitive data for non-admin users accessing their own profile
                 $user_arr = array(
                     "id" => $user->id,
                     "username" => $user->username,
@@ -35,11 +54,15 @@ switch($request_method) {
                     "is_active" => $user->is_active,
                     "last_login" => $user->last_login,
                     "created_at" => $user->created_at,
-                    "updated_at" => $user->updated_at,
-                    "role_badge" => $user->getRoleBadge(),
-                    "status_badge" => $user->getStatusBadge()
+                    "updated_at" => $user->updated_at
                 );
                 
+                // Add admin-only data if current user is admin
+                if ($auth->isAdmin($current_user)) {
+                    $user_arr["role_badge"] = $user->getRoleBadge();
+                    $user_arr["status_badge"] = $user->getStatusBadge();
+                }
+                
                 http_response_code(200);
                 echo json_encode($user_arr);
             } else {
@@ -47,6 +70,11 @@ switch($request_method) {
                 echo json_encode(array("message" => "User not found."));
             }
         } else {
+            // List all users - admin only
+            if (!$auth->isAdmin($current_user)) {
+                $auth->sendForbiddenResponse();
+            }
+            
             $stmt = $user->read();
             $num = $stmt->rowCount();
             
@@ -85,6 +113,11 @@ switch($request_method) {
         break;
         
     case 'POST':
+        // Only admins can create users
+        if (!$auth->isAdmin($current_user)) {
+            $auth->sendForbiddenResponse();
+        }
+        
         $data = json_decode(file_get_contents("php://input"));
         
         if(!empty($data->username) && !empty($data->email) && !empty($data->first_name) && !empty($data->last_name) && !empty($data->password)) {
@@ -129,13 +162,31 @@ switch($request_method) {
         $data = json_decode(file_get_contents("php://input"));
         
         if(!empty($data->id) && !empty($data->username) && !empty($data->email) && !empty($data->first_name) && !empty($data->last_name)) {
-            $user->id = $data->id;
+            $user_id = $data->id;
+            
+            // Check authorization: Admin can update any user, users can only update their own profile
+            if (!$auth->canAccess($current_user, null, $user_id)) {
+                $auth->sendForbiddenResponse();
+            }
+            
+            $user->id = $user_id;
             $user->username = $data->username;
             $user->email = $data->email;
             $user->first_name = $data->first_name;
             $user->last_name = $data->last_name;
-            $user->role = $data->role ?? 'user';
-            $user->is_active = $data->is_active ?? true;
+            
+            // Only admins can change role and is_active status
+            if ($auth->isAdmin($current_user)) {
+                $user->role = $data->role ?? 'user';
+                $user->is_active = $data->is_active ?? true;
+            } else {
+                // For regular users, preserve existing role and active status
+                $existing_user = new User($db);
+                $existing_user->id = $user_id;
+                $existing_user->readOne();
+                $user->role = $existing_user->role;
+                $user->is_active = $existing_user->is_active;
+            }
             
             // Update password if provided
             $update_password = !empty($data->password);
@@ -157,8 +208,22 @@ switch($request_method) {
         break;
         
     case 'DELETE':
+        // Only admins can delete users
+        if (!$auth->isAdmin($current_user)) {
+            $auth->sendForbiddenResponse();
+        }
+        
         if(isset($_GET['id'])) {
-            $user->id = $_GET['id'];
+            $user_id = $_GET['id'];
+            
+            // Prevent admin from deleting themselves
+            if ($user_id == $current_user['id']) {
+                http_response_code(400);
+                echo json_encode(array("message" => "Cannot delete your own account."));
+                break;
+            }
+            
+            $user->id = $user_id;
             
             if($user->delete()) {
                 http_response_code(200);

+ 173 - 0
backend/middleware/auth.php

@@ -0,0 +1,173 @@
+<?php
+/**
+ * Authentication and Authorization Middleware
+ * Handles user authentication and role-based access control
+ */
+
+class AuthMiddleware {
+    private $db;
+    private $user;
+    
+    public function __construct($db) {
+        $this->db = $db;
+        require_once __DIR__ . '/../models/User.php';
+        $this->user = new User($db);
+    }
+    
+    /**
+     * Authenticate user from JWT token or session
+     * @return array|null User data or null if not authenticated
+     */
+    public function authenticate() {
+        // Check for Authorization header
+        $headers = getallheaders();
+        $auth_header = $headers['Authorization'] ?? $headers['authorization'] ?? '';
+        
+        if ($auth_header) {
+            // Extract token from "Bearer <token>" format
+            $token = str_replace('Bearer ', '', $auth_header);
+            return $this->validateToken($token);
+        }
+        
+        // Fallback to session-based authentication
+        if (session_status() === PHP_SESSION_NONE) {
+            session_start();
+        }
+        
+        if (isset($_SESSION['user_id'])) {
+            $this->user->id = $_SESSION['user_id'];
+            $this->user->readOne();
+            
+            if ($this->user->username) {
+                return [
+                    'id' => $this->user->id,
+                    'username' => $this->user->username,
+                    'email' => $this->user->email,
+                    'first_name' => $this->user->first_name,
+                    'last_name' => $this->user->last_name,
+                    'role' => $this->user->role,
+                    'is_active' => $this->user->is_active
+                ];
+            }
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Validate JWT token (simplified implementation)
+     * @param string $token JWT token
+     * @return array|null User data or null if invalid
+     */
+    private function validateToken($token) {
+        // For now, implement a simple token validation
+        // In production, use a proper JWT library
+        try {
+            $payload = json_decode(base64_decode($token), true);
+            
+            if (isset($payload['user_id']) && isset($payload['expires'])) {
+                if ($payload['expires'] > time()) {
+                    $this->user->id = $payload['user_id'];
+                    $this->user->readOne();
+                    
+                    if ($this->user->username && $this->user->is_active) {
+                        return [
+                            'id' => $this->user->id,
+                            'username' => $this->user->username,
+                            'email' => $this->user->email,
+                            'first_name' => $this->user->first_name,
+                            'last_name' => $this->user->last_name,
+                            'role' => $this->user->role,
+                            'is_active' => $this->user->is_active
+                        ];
+                    }
+                }
+            }
+        } catch (Exception $e) {
+            // Token is invalid
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Check if user has admin role
+     * @param array $user User data
+     * @return bool True if user is admin
+     */
+    public function isAdmin($user) {
+        return $user && $user['role'] === 'admin';
+    }
+    
+    /**
+     * Check if user can access resource
+     * @param array $user User data
+     * @param string $required_role Required role (admin/user)
+     * @param int|null $resource_owner_id Resource owner ID (for profile access)
+     * @return bool True if user can access
+     */
+    public function canAccess($user, $required_role = null, $resource_owner_id = null) {
+        if (!$user || !$user['is_active']) {
+            return false;
+        }
+        
+        // Admin can access everything
+        if ($user['role'] === 'admin') {
+            return true;
+        }
+        
+        // Check if specific role is required
+        if ($required_role && $user['role'] !== $required_role) {
+            return false;
+        }
+        
+        // Check if user is accessing their own resource (profile management)
+        if ($resource_owner_id && $user['id'] == $resource_owner_id) {
+            return true;
+        }
+        
+        // Default: allow access for regular users to their own resources
+        return !$resource_owner_id;
+    }
+    
+    /**
+     * Generate simple JWT token
+     * @param array $user User data
+     * @return string JWT token
+     */
+    public function generateToken($user) {
+        $payload = [
+            'user_id' => $user['id'],
+            'username' => $user['username'],
+            'role' => $user['role'],
+            'expires' => time() + (24 * 60 * 60) // 24 hours
+        ];
+        
+        return base64_encode(json_encode($payload));
+    }
+    
+    /**
+     * Send unauthorized response
+     */
+    public function sendUnauthorizedResponse() {
+        http_response_code(401);
+        echo json_encode([
+            'success' => false,
+            'message' => 'Unauthorized access. Please login.'
+        ]);
+        exit;
+    }
+    
+    /**
+     * Send forbidden response
+     */
+    public function sendForbiddenResponse() {
+        http_response_code(403);
+        echo json_encode([
+            'success' => false,
+            'message' => 'Access denied. Insufficient permissions.'
+        ]);
+        exit;
+    }
+}
+?>

+ 1 - 0
frontend/index.html

@@ -4,6 +4,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Inventory Management</title>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
     <style>
         * {
             margin: 0;

+ 118 - 56
frontend/src/App.vue

@@ -42,7 +42,9 @@
       <div v-if="isLoggedIn">
         <NavigationTabs 
           :active-section="activeSection"
+          :current-user="currentUser"
           @section-change="activeSection = $event"
+          @logout="logout"
         />
       
       <!-- Items Section -->
@@ -247,6 +249,22 @@
         v-if="activeSection === 'tasks'"
         :active-section="activeSection"
       />
+      
+      <!-- User Management Section (Admin Only) -->
+      <UserManagement
+        v-if="activeSection === 'user-management' && currentUser && currentUser.role === 'admin'"
+        :current-user="currentUser"
+        @success="showSuccess"
+        @error="showError"
+      />
+      
+      <!-- Profile Management Section (All Users) -->
+      <ProfileManagement
+        v-if="activeSection === 'profile' && currentUser"
+        :current-user="currentUser"
+        @success="showSuccess"
+        @error="showError"
+      />
       </div>
     </div>
     </header>
@@ -1473,7 +1491,7 @@
 </template>
 
 <script>
-import axios from './api/axios'
+import api from './api/axios'
 import { formatDate, formatCurrency, formatDateTime } from './utils/locale.js'
 import NavigationTabs from './components/common/NavigationTabs.vue'
 import ProjectsSection from './components/projects/ProjectsSection.vue'
@@ -1484,6 +1502,8 @@ import ExcelAccountingSection from './components/accounting/ExcelAccountingSecti
 import TuloslaskelmaSection from './components/accounting/TuloslaskelmaSection.vue'
 import ALVLaskentaSection from './components/accounting/ALVLaskentaSection.vue'
 import TaskManagementSection from './components/projects/TaskManagementSection.vue'
+import UserManagement from './components/UserManagement.vue'
+import ProfileManagement from './components/ProfileManagement.vue'
 
 export default {
   name: 'App',
@@ -1496,7 +1516,9 @@ export default {
     ExcelAccountingSection,
     TuloslaskelmaSection,
     ALVLaskentaSection,
-    TaskManagementSection
+    TaskManagementSection,
+    UserManagement,
+    ProfileManagement
   },
   data() {
     return {
@@ -1698,7 +1720,7 @@ export default {
     async fetchItems() {
       this.loading = true
       try {
-        const response = await axios.get('/api/items.php')
+        const response = await api.get('/items.php')
         this.items = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching items: ' + error.message, 'error')
@@ -1710,10 +1732,10 @@ export default {
     async saveItem() {
       try {
         if (this.isEditing) {
-          await axios.put('/api/items.php', this.formData)
+          await api.put('/items.php', this.formData)
           this.showMessage('Item updated successfully!', 'success')
         } else {
-          await axios.post('/api/items.php', this.formData)
+          await api.post('/items.php', this.formData)
           this.showMessage('Item added successfully!', 'success')
         }
         
@@ -1730,13 +1752,22 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/items.php?id=${id}`)
+        await api.delete(`/items.php?id=${id}`)
         this.showMessage('Item deleted successfully!', 'success')
         this.fetchItems()
       } catch (error) {
         this.showMessage('Error deleting item: ' + error.message, 'error')
       }
     },
+    
+    // User management methods
+    showSuccess(message) {
+      this.showMessage(message, 'success')
+    },
+    
+    showError(message) {
+      this.showMessage(message, 'error')
+    },
 
     showAddModal() {
       this.isEditing = false
@@ -1789,7 +1820,7 @@ export default {
       formData.append('picture', file)
       
       try {
-        const response = await axios.post('/api/upload.php', formData, {
+        const response = await api.post('/upload.php', formData, {
           headers: {
             'Content-Type': 'multipart/form-data'
           }
@@ -1838,7 +1869,7 @@ export default {
     
     async fetchRentalPrices(itemId) {
       try {
-        const response = await axios.get(`/api/rental_prices.php?item_id=${itemId}`)
+        const response = await api.get(`/api/rental_prices.php?item_id=${itemId}`)
         this.rentalPrices = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching rental prices: ' + error.message, 'error')
@@ -1847,7 +1878,7 @@ export default {
     
     async fetchAttachments(itemId) {
       try {
-        const response = await axios.get(`/api/attachments.php?item_id=${itemId}`)
+        const response = await api.get(`/api/attachments.php?item_id=${itemId}`)
         this.attachments = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching attachments: ' + error.message, 'error')
@@ -1878,7 +1909,7 @@ export default {
     
     async saveRentalPrice() {
       try {
-        await axios.post('/api/rental_prices.php', this.rentalForm)
+        await api.post('/api/rental_prices.php', this.rentalForm)
         this.showMessage('Rental price added successfully!', 'success')
         this.closeRentalModal()
         await this.fetchRentalPrices(this.selectedItem.id)
@@ -1893,7 +1924,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/rental_prices.php?id=${id}`)
+        await api.delete(`/api/rental_prices.php?id=${id}`)
         this.showMessage('Rental price deleted successfully!', 'success')
         await this.fetchRentalPrices(this.selectedItem.id)
       } catch (error) {
@@ -1935,7 +1966,7 @@ export default {
         formData.append('item_id', this.attachmentForm.item_id)
         formData.append('file_type', this.attachmentForm.file_type)
         
-        await axios.post('/api/attachments.php', formData, {
+        await api.post('/api/attachments.php', formData, {
           headers: {
             'Content-Type': 'multipart/form-data'
           }
@@ -1955,7 +1986,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/attachments.php?id=${id}`)
+        await api.delete(`/api/attachments.php?id=${id}`)
         this.showMessage('Attachment deleted successfully!', 'success')
         await this.fetchAttachments(this.selectedItem.id)
       } catch (error) {
@@ -1982,7 +2013,7 @@ export default {
     async fetchClients() {
       this.clientsLoading = true
       try {
-        const response = await axios.get('/api/clients.php')
+        const response = await api.get('/clients.php')
         this.clients = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching clients: ' + error.message, 'error')
@@ -1999,7 +2030,7 @@ export default {
       
       this.clientsLoading = true
       try {
-        const response = await axios.get(`/api/clients.php?search=${this.clientSearch}`)
+        const response = await api.get(`/api/clients.php?search=${this.clientSearch}`)
         this.clients = response.data.records || []
       } catch (error) {
         this.showMessage('Error searching clients: ' + error.message, 'error')
@@ -2073,10 +2104,10 @@ export default {
     async saveClient() {
       try {
         if (this.isEditingClient) {
-          await axios.put('/api/clients.php', this.clientForm)
+          await api.put('/api/clients.php', this.clientForm)
           this.showMessage('Client updated successfully!', 'success')
         } else {
-          await axios.post('/api/clients.php', this.clientForm)
+          await api.post('/api/clients.php', this.clientForm)
           this.showMessage('Client added successfully!', 'success')
         }
         
@@ -2093,7 +2124,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/clients.php?id=${id}`)
+        await api.delete(`/api/clients.php?id=${id}`)
         this.showMessage('Client deleted successfully!', 'success')
         await this.fetchClients()
       } catch (error) {
@@ -2109,7 +2140,7 @@ export default {
     async fetchContactPersons(customerId) {
       this.contactPersonsLoading = true
       try {
-        const response = await axios.get(`/api/contact_persons.php?client_id=${customerId}`)
+        const response = await api.get(`/api/contact_persons.php?client_id=${customerId}`)
         this.contactPersons = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching contact persons: ' + error.message, 'error')
@@ -2165,10 +2196,10 @@ export default {
     async saveContactPerson() {
       try {
         if (this.isEditingContactPerson) {
-          await axios.put('/api/contact_persons.php', this.contactPersonForm)
+          await api.put('/api/contact_persons.php', this.contactPersonForm)
           this.showMessage('Contact person updated successfully!', 'success')
         } else {
-          await axios.post('/api/contact_persons.php', this.contactPersonForm)
+          await api.post('/api/contact_persons.php', this.contactPersonForm)
           this.showMessage('Contact person added successfully!', 'success')
         }
         
@@ -2185,7 +2216,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/contact_persons.php?id=${id}`)
+        await api.delete(`/api/contact_persons.php?id=${id}`)
         this.showMessage('Contact person deleted successfully!', 'success')
         await this.fetchContactPersons(this.selectedClient ? this.selectedClient.id : null)
       } catch (error) {
@@ -2197,7 +2228,7 @@ export default {
     async fetchInvoices() {
       this.invoicesLoading = true
       try {
-        const response = await axios.get('/api/invoices.php')
+        const response = await api.get('/api/invoices.php')
         this.invoices = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching invoices: ' + error.message, 'error')
@@ -2214,7 +2245,7 @@ export default {
       
       this.invoicesLoading = true
       try {
-        const response = await axios.get(`/api/invoices.php?search=${this.invoiceSearch}`)
+        const response = await api.get(`/api/invoices.php?search=${this.invoiceSearch}`)
         this.invoices = response.data.records || []
       } catch (error) {
         this.showMessage('Error searching invoices: ' + error.message, 'error')
@@ -2276,10 +2307,10 @@ export default {
     async saveInvoice() {
       try {
         if (this.isEditingInvoice) {
-          await axios.put('/api/invoices.php', this.invoiceForm)
+          await api.put('/api/invoices.php', this.invoiceForm)
           this.showMessage('Invoice updated successfully!', 'success')
         } else {
-          await axios.post('/api/invoices.php', this.invoiceForm)
+          await api.post('/api/invoices.php', this.invoiceForm)
           this.showMessage('Invoice created successfully!', 'success')
         }
         
@@ -2296,7 +2327,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/invoices.php?id=${id}`)
+        await api.delete(`/api/invoices.php?id=${id}`)
         this.showMessage('Invoice deleted successfully!', 'success')
         await this.fetchInvoices()
       } catch (error) {
@@ -2318,10 +2349,10 @@ export default {
     async saveInvoiceItem() {
       try {
         if (this.invoiceItemsForm.id) {
-          await axios.put('/api/invoice_items.php', this.invoiceItemsForm)
+          await api.put('/api/invoice_items.php', this.invoiceItemsForm)
           this.showMessage('Invoice item updated successfully!', 'success')
         } else {
-          await axios.post('/api/invoice_items.php', this.invoiceItemsForm)
+          await api.post('/api/invoice_items.php', this.invoiceItemsForm)
           this.showMessage('Invoice item added successfully!', 'success')
         }
         
@@ -2338,7 +2369,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/invoice_items.php?id=${id}`)
+        await api.delete(`/api/invoice_items.php?id=${id}`)
         this.showMessage('Invoice item deleted successfully!', 'success')
         await this.viewInvoice(this.selectedInvoice)
       } catch (error) {
@@ -2351,7 +2382,7 @@ export default {
       this.loginError = ''
       
       try {
-        const response = await axios.post('/api/auth.php', {
+        const response = await api.post('/auth.php', {
           action: 'login',
           username: this.loginForm.username,
           password: this.loginForm.password
@@ -2375,15 +2406,46 @@ export default {
       }
     },
     
-    logout() {
-      this.isLoggedIn = false
-      this.currentUser = null
-      this.showMessage('Logged out successfully!', 'success')
+    async logout() {
+      try {
+        // Call backend logout endpoint to clear PHP session
+        await api.post('/auth.php', { action: 'logout' })
+        
+        // Clear frontend data
+        this.isLoggedIn = false
+        this.currentUser = null
+        
+        // Clear any stored authentication data
+        localStorage.removeItem('authToken')
+        sessionStorage.removeItem('authToken')
+        sessionStorage.removeItem('currentUser')
+        
+        // Clear application data
+        this.items = []
+        this.clients = []
+        this.projects = []
+        this.users = []
+        this.invoices = []
+        this.accounts = []
+        this.journalEntries = []
+        this.transactions = []
+        
+        this.showMessage('Logged out successfully!', 'success')
+      } catch (error) {
+        console.error('Logout error:', error)
+        // Still clear frontend data even if backend call fails
+        this.isLoggedIn = false
+        this.currentUser = null
+        localStorage.removeItem('authToken')
+        sessionStorage.removeItem('authToken')
+        sessionStorage.removeItem('currentUser')
+        this.showMessage('Logged out successfully!', 'success')
+      }
     },
     
     async checkAuthStatus() {
       try {
-        const response = await axios.get('/api/auth.php?action=status')
+        const response = await api.get('/auth.php?action=status')
         if (response.status === 200 && response.data.user) {
           this.isLoggedIn = true
           this.currentUser = response.data.user
@@ -2431,7 +2493,7 @@ export default {
     async fetchProjects() {
       this.projectsLoading = true
       try {
-        const response = await axios.get('/api/projects.php')
+        const response = await api.get('/projects.php')
         this.projects = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching projects: ' + error.message, 'error')
@@ -2444,7 +2506,7 @@ export default {
       this.projectSearch = searchTerm
       this.projectsLoading = true
       try {
-        const response = await axios.get(`/api/projects.php?search=${this.projectSearch}`)
+        const response = await api.get(`/api/projects.php?search=${this.projectSearch}`)
         this.projects = response.data.records || []
       } catch (error) {
         this.showMessage('Error searching projects: ' + error.message, 'error')
@@ -2485,7 +2547,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/projects.php?id=${id}`)
+        await api.delete(`/api/projects.php?id=${id}`)
         this.showMessage('Project deleted successfully!', 'success')
         this.fetchProjects()
       } catch (error) {
@@ -2497,7 +2559,7 @@ export default {
     async fetchUsers() {
       this.usersLoading = true
       try {
-        const response = await axios.get('/api/users.php')
+        const response = await api.get('/api/users.php')
         this.users = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching users: ' + error.message, 'error')
@@ -2510,7 +2572,7 @@ export default {
       this.userSearch = searchTerm
       this.usersLoading = true
       try {
-        const response = await axios.get(`/api/users.php?search=${this.userSearch}`)
+        const response = await api.get(`/api/users.php?search=${this.userSearch}`)
         this.users = response.data.records || []
       } catch (error) {
         this.showMessage('Error searching users: ' + error.message, 'error')
@@ -2559,7 +2621,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/users.php?id=${id}`)
+        await api.delete(`/api/users.php?id=${id}`)
         this.showMessage('User deleted successfully!', 'success')
         this.fetchUsers()
       } catch (error) {
@@ -2602,11 +2664,11 @@ export default {
         let response
         if (this.userForm.id) {
           // Update existing user
-          response = await axios.put(`/api/users.php?id=${this.userForm.id}`, userData)
+          response = await api.put(`/api/users.php?id=${this.userForm.id}`, userData)
           this.showMessage('User updated successfully!', 'success')
         } else {
           // Create new user
-          response = await axios.post('/api/users.php', userData)
+          response = await api.post('/api/users.php', userData)
           this.showMessage('User created successfully!', 'success')
         }
         
@@ -2648,11 +2710,11 @@ export default {
         let response
         if (this.projectForm.id) {
           // Update existing project
-          response = await axios.put(`/api/projects.php?id=${this.projectForm.id}`, projectData)
+          response = await api.put(`/api/projects.php?id=${this.projectForm.id}`, projectData)
           this.showMessage('Project updated successfully!', 'success')
         } else {
           // Create new project
-          response = await axios.post('/api/projects.php', projectData)
+          response = await api.post('/api/projects.php', projectData)
           this.showMessage('Project created successfully!', 'success')
         }
         
@@ -2682,7 +2744,7 @@ export default {
     
     async fetchCustomerContacts(customerId) {
       try {
-        const response = await axios.get(`/api/contact_persons.php?client_id=${customerId}`)
+        const response = await api.get(`/api/contact_persons.php?client_id=${customerId}`)
         this.customerContacts = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching contacts: ' + error.message, 'error')
@@ -2703,7 +2765,7 @@ export default {
           notes: this.contactForm.notes
         }
         
-        await axios.post('/api/contact_persons.php', contactData)
+        await api.post('/api/contact_persons.php', contactData)
         this.showMessage('Contact added successfully!', 'success')
         this.resetContactForm()
         await this.fetchCustomerContacts(this.selectedCustomer.id)
@@ -2732,7 +2794,7 @@ export default {
           notes: this.contactForm.notes
         }
         
-        await axios.put(`/api/contact_persons.php?id=${this.contactForm.id}`, contactData)
+        await api.put(`/api/contact_persons.php?id=${this.contactForm.id}`, contactData)
         this.showMessage('Contact updated successfully!', 'success')
         this.resetContactForm()
         await this.fetchCustomerContacts(this.selectedCustomer.id)
@@ -2748,7 +2810,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/contact_persons.php?id=${contactId}`)
+        await api.delete(`/api/contact_persons.php?id=${contactId}`)
         this.showMessage('Contact deleted successfully!', 'success')
         await this.fetchCustomerContacts(this.selectedCustomer.id)
         await this.fetchClients()
@@ -2783,7 +2845,7 @@ export default {
     async fetchAccounts() {
       this.accountsLoading = true
       try {
-        const response = await axios.get('/api/chart_of_accounts.php')
+        const response = await api.get('/api/chart_of_accounts.php')
         this.accounts = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching accounts: ' + error.message, 'error')
@@ -2796,7 +2858,7 @@ export default {
       this.accountSearch = searchTerm
       this.accountsLoading = true
       try {
-        const response = await axios.get(`/api/chart_of_accounts.php?search=${this.accountSearch}`)
+        const response = await api.get(`/api/chart_of_accounts.php?search=${this.accountSearch}`)
         this.accounts = response.data.records || []
       } catch (error) {
         this.showMessage('Error searching accounts: ' + error.message, 'error')
@@ -2837,7 +2899,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/chart_of_accounts.php?id=${id}`)
+        await api.delete(`/api/chart_of_accounts.php?id=${id}`)
         this.showMessage('Account deleted successfully!', 'success')
         this.fetchAccounts()
       } catch (error) {
@@ -2849,7 +2911,7 @@ export default {
     async fetchJournalEntries() {
       this.journalEntriesLoading = true
       try {
-        const response = await axios.get('/api/journal_entries.php')
+        const response = await api.get('/api/journal_entries.php')
         this.journalEntries = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching journal entries: ' + error.message, 'error')
@@ -2862,7 +2924,7 @@ export default {
     async fetchTransactions() {
       this.transactionsLoading = true
       try {
-        const response = await axios.get('/api/account_transactions.php')
+        const response = await api.get('/api/account_transactions.php')
         this.transactions = response.data.records || []
       } catch (error) {
         this.showMessage('Error fetching transactions: ' + error.message, 'error')
@@ -2895,7 +2957,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/journal_entries.php?id=${id}`)
+        await api.delete(`/api/journal_entries.php?id=${id}`)
         this.showMessage('Journal entry deleted successfully!', 'success')
         this.fetchJournalEntries()
       } catch (error) {

+ 17 - 1
frontend/src/api/axios.js

@@ -2,13 +2,28 @@ import axios from 'axios'
 
 // Create axios instance with base URL
 const api = axios.create({
-  baseURL: import.meta.env.VUE_APP_API_URL || '',
+  baseURL: '/api',
   timeout: 10000,
   headers: {
     'Content-Type': 'application/json'
   }
 })
 
+// Add request interceptor to include authentication token
+api.interceptors.request.use(
+  config => {
+    // Add authentication token if available
+    const token = localStorage.getItem('authToken') || sessionStorage.getItem('authToken')
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`
+    }
+    return config
+  },
+  error => {
+    return Promise.reject(error)
+  }
+)
+
 // Add response interceptor to handle authentication errors
 api.interceptors.response.use(
   response => response,
@@ -23,6 +38,7 @@ api.interceptors.response.use(
     if (error.response && error.response.status === 401) {
       // Clear any stored auth data
       localStorage.removeItem('authToken')
+      sessionStorage.removeItem('authToken')
       sessionStorage.removeItem('currentUser')
       
       // Emit custom event for app to handle

+ 10 - 8
frontend/src/components/HeaderTimer.vue

@@ -108,7 +108,7 @@
 </template>
 
 <script>
-import axios from '../api/axios'
+import api from '../api/axios'
 import TimerManagement from './TimerManagement.vue'
 
 export default {
@@ -123,7 +123,8 @@ export default {
       tasks: [],
       selectedTask: null,
       selectedTaskId: '',
-      loading: false
+      loading: false,
+      currentTime: Date.now()
     }
   },
   computed: {
@@ -140,7 +141,8 @@ export default {
     // Update timer every second
     this.timerInterval = setInterval(() => {
       if (this.hasActiveTimer) {
-        this.$forceUpdate() // Force update to refresh timer display
+        // Update current time to trigger timer duration recalculation
+        this.currentTime = Date.now()
       }
     }, 1000)
   },
@@ -152,7 +154,7 @@ export default {
   methods: {
     async loadActiveTimers() {
       try {
-        const response = await axios.get('/api/timers.php?action=active')
+        const response = await api.get('/timers.php?action=active')
         if (response.data.success) {
           this.activeTimers = response.data.data || []
         } else {
@@ -166,7 +168,7 @@ export default {
     
     async loadTasks() {
       try {
-        const response = await axios.get('/api/tasks.php')
+        const response = await api.get('/tasks.php')
         if (response.data.success) {
           this.tasks = response.data.data || []
         }
@@ -198,7 +200,7 @@ export default {
     
     async startTimerDirect() {
       try {
-        const response = await axios.post('/api/timers.php', {
+        const response = await api.post('/timers.php', {
           action: 'start',
           task_id: null,
           user_id: 1,
@@ -219,7 +221,7 @@ export default {
       try {
         const taskDescription = this.selectedTask ? `Timer started for ${this.selectedTask.title}` : 'Timer started without task'
         
-        const response = await axios.post('/api/timers.php', {
+        const response = await api.post('/timers.php', {
           action: 'start',
           task_id: this.selectedTask ? this.selectedTask.id : null,
           user_id: 1, // Default to current user
@@ -239,7 +241,7 @@ export default {
     
     async stopTimer(timerId) {
       try {
-        const response = await axios.post('/api/timer_stop.php', {
+        const response = await api.post('/timer_stop.php', {
           action: 'stop',
           id: timerId
         })

+ 391 - 0
frontend/src/components/ProfileManagement.vue

@@ -0,0 +1,391 @@
+<template>
+  <div class="profile-management">
+    <div class="d-flex justify-content-between align-items-center mb-4">
+      <h2>Profiilin hallinta</h2>
+    </div>
+
+    <div class="row">
+      <div class="col-md-8">
+        <!-- Profile Form -->
+        <div class="card">
+          <div class="card-header">
+            <h5 class="mb-0">Perustiedot</h5>
+          </div>
+          <div class="card-body">
+            <form @submit.prevent="saveProfile">
+              <div class="row">
+                <div class="col-md-6 mb-3">
+                  <label for="username" class="form-label">Käyttäjänimi</label>
+                  <input 
+                    type="text" 
+                    class="form-control" 
+                    id="username" 
+                    v-model="formData.username" 
+                    required
+                    disabled
+                  >
+                  <small class="form-text text-muted">Käyttäjänimeä ei voi muuttaa</small>
+                </div>
+                <div class="col-md-6 mb-3">
+                  <label for="email" class="form-label">Sähköposti</label>
+                  <input 
+                    type="email" 
+                    class="form-control" 
+                    id="email" 
+                    v-model="formData.email" 
+                    required
+                  >
+                </div>
+              </div>
+              
+              <div class="row">
+                <div class="col-md-6 mb-3">
+                  <label for="firstName" class="form-label">Etunimi</label>
+                  <input 
+                    type="text" 
+                    class="form-control" 
+                    id="firstName" 
+                    v-model="formData.first_name" 
+                    required
+                  >
+                </div>
+                <div class="col-md-6 mb-3">
+                  <label for="lastName" class="form-label">Sukunimi</label>
+                  <input 
+                    type="text" 
+                    class="form-control" 
+                    id="lastName" 
+                    v-model="formData.last_name" 
+                    required
+                  >
+                </div>
+              </div>
+              
+              <div class="mb-3">
+                <label for="role" class="form-label">Rooli</label>
+                <input 
+                  type="text" 
+                  class="form-control" 
+                  id="role" 
+                  :value="getRoleLabel(formData.role)" 
+                  disabled
+                >
+                <small class="form-text text-muted">Roolin voi muuttaa vain ylläpitäjä</small>
+              </div>
+              
+              <div class="d-flex justify-content-between">
+                <div>
+                  <h6>Tilin tiedot</h6>
+                  <p class="text-muted">
+                    <strong>Luotu:</strong> {{ formatDate(formData.created_at) }}<br>
+                    <strong>Päivitetty:</strong> {{ formatDate(formData.updated_at) }}<br>
+                    <strong>Viimeksi kirjautunut:</strong> {{ formatDate(formData.last_login) }}
+                  </p>
+                </div>
+                <div>
+                  <button type="submit" class="btn btn-primary" :disabled="loading">
+                    <span v-if="loading" class="spinner-border spinner-border-sm me-2"></span>
+                    Tallenna muutokset
+                  </button>
+                </div>
+              </div>
+            </form>
+          </div>
+        </div>
+      </div>
+      
+      <div class="col-md-4">
+        <!-- Password Change -->
+        <div class="card">
+          <div class="card-header">
+            <h5 class="mb-0">Salasanan vaihto</h5>
+          </div>
+          <div class="card-body">
+            <form @submit.prevent="changePassword">
+              <div class="mb-3">
+                <label for="currentPassword" class="form-label">Nykyinen salasana</label>
+                <input 
+                  type="password" 
+                  class="form-control" 
+                  id="currentPassword" 
+                  v-model="passwordForm.current_password" 
+                  required
+                >
+              </div>
+              
+              <div class="mb-3">
+                <label for="newPassword" class="form-label">Uusi salasana</label>
+                <input 
+                  type="password" 
+                  class="form-control" 
+                  id="newPassword" 
+                  v-model="passwordForm.password" 
+                  required
+                  minlength="8"
+                >
+                <small class="form-text text-muted">Vähintään 8 merkkiä</small>
+              </div>
+              
+              <div class="mb-3">
+                <label for="confirmPassword" class="form-label">Vahvista uusi salasana</label>
+                <input 
+                  type="password" 
+                  class="form-control" 
+                  id="confirmPassword" 
+                  v-model="passwordForm.confirm_password" 
+                  required
+                >
+              </div>
+              
+              <button type="submit" class="btn btn-warning w-100" :disabled="passwordLoading">
+                <span v-if="passwordLoading" class="spinner-border spinner-border-sm me-2"></span>
+                Vaihda salasana
+              </button>
+            </form>
+          </div>
+        </div>
+        
+        <!-- Account Status -->
+        <div class="card mt-3">
+          <div class="card-header">
+            <h5 class="mb-0">Tilin tila</h5>
+          </div>
+          <div class="card-body">
+            <div class="d-flex align-items-center">
+              <span :class="getStatusBadgeClass(formData.is_active)" class="me-2">
+                {{ formData.is_active ? 'Aktiivinen' : 'Passiivinen' }}
+              </span>
+              <small class="text-muted" v-if="!formData.is_active">
+                Tili on passiivinen. Ota yhteyttä ylläpitäjään.
+              </small>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import api from '../api/axios'
+
+export default {
+  name: 'ProfileManagement',
+  props: {
+    currentUser: {
+      type: Object,
+      default: null
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      passwordLoading: false,
+      formData: {
+        id: null,
+        username: '',
+        email: '',
+        first_name: '',
+        last_name: '',
+        role: '',
+        is_active: false,
+        created_at: null,
+        updated_at: null,
+        last_login: null
+      },
+      passwordForm: {
+        current_password: '',
+        password: '',
+        confirm_password: ''
+      }
+    }
+  },
+  
+  async mounted() {
+    await this.loadProfile()
+  },
+  
+  methods: {
+    async loadProfile() {
+      this.loading = true
+      try {
+        // Use currentUser prop instead of making API call
+        if (!this.currentUser || !this.currentUser.id) {
+          this.$emit('error', 'Käyttäjätietoja ei löytynyt')
+          return
+        }
+        
+        // Load full user profile
+        const profileResponse = await api.get(`/users.php?id=${this.currentUser.id}`)
+        this.formData = profileResponse.data
+      } catch (error) {
+        console.error('Error loading profile:', error)
+        this.$emit('error', 'Profiilin lataaminen epäonnistui')
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    async saveProfile() {
+      this.loading = true
+      try {
+        await api.put('/users', this.formData)
+        this.$emit('success', 'Profiili päivitetty onnistuneesti')
+        await this.loadProfile()
+      } catch (error) {
+        console.error('Error saving profile:', error)
+        const message = error.response?.data?.message || 'Profiilin tallentaminen epäonnistui'
+        this.$emit('error', message)
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    async changePassword() {
+      if (this.passwordForm.password !== this.passwordForm.confirm_password) {
+        this.$emit('error', 'Salasanat eivät täsmää')
+        return
+      }
+      
+      if (this.passwordForm.password.length < 8) {
+        this.$emit('error', 'Salasanan tulee olla vähintään 8 merkkiä pitkä')
+        return
+      }
+      
+      this.passwordLoading = true
+      try {
+        await api.put('/users', {
+          ...this.formData,
+          password: this.passwordForm.password,
+          current_password: this.passwordForm.current_password
+        })
+        
+        this.$emit('success', 'Salasana vaihdettu onnistuneesti')
+        
+        // Clear password form
+        this.passwordForm = {
+          current_password: '',
+          password: '',
+          confirm_password: ''
+        }
+      } catch (error) {
+        console.error('Error changing password:', error)
+        const message = error.response?.data?.message || 'Salasanan vaihtaminen epäonnistui'
+        this.$emit('error', message)
+      } finally {
+        this.passwordLoading = false
+      }
+    },
+    
+    getRoleLabel(role) {
+      return role === 'admin' ? 'Ylläpitäjä' : 'Käyttäjä'
+    },
+    
+    getStatusBadgeClass(isActive) {
+      return isActive ? 'badge bg-success' : 'badge bg-secondary'
+    },
+    
+    formatDate(dateString) {
+      if (!dateString) return 'Ei koskaan'
+      return new Date(dateString).toLocaleString('fi-FI')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.profile-management {
+  padding: 20px;
+  color: #333 !important;
+}
+
+.card {
+  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+  background-color: #ffffff;
+  color: #333;
+}
+
+.card-header {
+  background-color: #f8f9fa;
+  border-bottom: 1px solid #dee2e6;
+  color: #495057;
+}
+
+.card-body {
+  color: #333;
+}
+
+.form-label {
+  color: #495057;
+  font-weight: 500;
+}
+
+.form-control {
+  color: #495057;
+  background-color: #ffffff;
+  border-color: #ced4da;
+}
+
+.form-control:disabled {
+  background-color: #e9ecef;
+  color: #6c757d;
+}
+
+.form-text {
+  font-size: 0.875em;
+  color: #6c757d !important;
+}
+
+.btn {
+  color: #ffffff;
+}
+
+.btn-primary {
+  background-color: #007bff;
+  border-color: #007bff;
+}
+
+.btn-primary:hover {
+  background-color: #0056b3;
+  border-color: #004085;
+}
+
+.btn-secondary {
+  background-color: #6c757d;
+  border-color: #6c757d;
+}
+
+.btn-secondary:hover {
+  background-color: #545b62;
+  border-color: #545b62;
+}
+
+.btn:disabled {
+  opacity: 0.65;
+  cursor: not-allowed;
+}
+
+h2, h5 {
+  color: white !important;
+}
+
+.text-muted {
+  color: #6c757d !important;
+}
+
+.alert {
+  color: #333;
+}
+
+.alert-success {
+  background-color: #d4edda;
+  border-color: #c3e6cb;
+  color: #155724;
+}
+
+.alert-danger {
+  background-color: #f8d7da;
+  border-color: #f5c6cb;
+  color: #721c24;
+}
+</style>

+ 9 - 9
frontend/src/components/TimerManagement.vue

@@ -282,7 +282,7 @@
 </template>
 
 <script>
-import axios from '../api/axios'
+import api from '../api/axios'
 
 export default {
   name: 'TimerManagement',
@@ -346,7 +346,7 @@ export default {
     async loadActiveTimers() {
       try {
         this.loading = true
-        const response = await axios.get('/api/timers.php?action=active')
+        const response = await api.get('/timers.php?action=active')
         if (response.data.success) {
           this.activeTimers = response.data.data || []
         }
@@ -369,7 +369,7 @@ export default {
           params.append('task_id', this.historyFilter.task_id)
         }
         
-        const response = await axios.get(`/api/timer_history.php?${params}`)
+        const response = await api.get(`/timer_history.php?${params}`)
         if (response.data.success) {
           this.timerHistory = response.data.data || []
         }
@@ -392,7 +392,7 @@ export default {
           params.append('task_id', this.workHoursFilter.task_id)
         }
         
-        const response = await axios.get(`/api/work_hours.php?${params}`)
+        const response = await api.get(`/work_hours.php?${params}`)
         if (response.data.success) {
           this.workHours = response.data.data || []
         }
@@ -405,7 +405,7 @@ export default {
     
     async loadTasks() {
       try {
-        const response = await axios.get('/api/tasks.php')
+        const response = await api.get('/tasks.php')
         if (response.data.success) {
           this.tasks = response.data.data || []
         }
@@ -416,7 +416,7 @@ export default {
     
     async stopTimer(timerId) {
       try {
-        const response = await axios.post('/api/timer_stop.php', {
+        const response = await api.post('/timer_stop.php', {
           action: 'stop',
           id: timerId
         })
@@ -447,7 +447,7 @@ export default {
     
     async saveTimer() {
       try {
-        const response = await axios.post('/api/timer_update.php', {
+        const response = await api.post('/timer_update.php', {
           action: 'update',
           id: this.editForm.id,
           description: this.editForm.description,
@@ -470,7 +470,7 @@ export default {
       if (!confirm('Oletko varma että haluat poistaa tämän ajastimen?')) return
       
       try {
-        const response = await axios.post('/api/timers.php', {
+        const response = await api.post('/timers.php', {
           action: 'delete',
           id: timerId
         })
@@ -494,7 +494,7 @@ export default {
       if (!confirm('Oletko varma että haluat poistaa tämän työtunnin?')) return
       
       try {
-        const response = await axios.delete(`/api/work_hours.php?id=${workHourId}`)
+        const response = await api.delete(`/work_hours.php?id=${workHourId}`)
         
         if (response.data.success) {
           this.loadWorkHours()

+ 534 - 0
frontend/src/components/UserManagement.vue

@@ -0,0 +1,534 @@
+<template>
+  <div class="user-management">
+    <div class="d-flex justify-content-between align-items-center mb-4">
+      <h2>Käyttäjien hallinta</h2>
+      <button class="btn btn-primary" @click="showCreateModal = true">
+        <i class="fas fa-plus"></i> Luo uusi käyttäjä
+      </button>
+    </div>
+
+    <!-- Users Table -->
+    <div class="card">
+      <div class="card-body">
+        <div v-if="loading" class="text-center">
+          <div class="spinner-border" role="status">
+            <span class="visually-hidden">Ladataan...</span>
+          </div>
+        </div>
+        
+        <div v-else-if="users.length === 0" class="text-center text-muted">
+          <i class="fas fa-users fa-3x mb-3"></i>
+          <p>Ei käyttäjiä löytynyt</p>
+        </div>
+        
+        <div v-else class="table-responsive">
+          <table class="table table-striped">
+            <thead>
+              <tr>
+                <th>Käyttäjänimi</th>
+                <th>Nimi</th>
+                <th>Sähköposti</th>
+                <th>Rooli</th>
+                <th>Tila</th>
+                <th>Viimeksi kirjautunut</th>
+                <th>Toiminnot</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr v-for="user in users" :key="user.id">
+                <td>{{ user.username }}</td>
+                <td>{{ user.first_name }} {{ user.last_name }}</td>
+                <td>{{ user.email }}</td>
+                <td>
+                  <span :class="getRoleBadgeClass(user.role)">
+                    {{ getRoleLabel(user.role) }}
+                  </span>
+                </td>
+                <td>
+                  <span :class="getStatusBadgeClass(user.is_active)">
+                    {{ user.is_active ? 'Aktiivinen' : 'Passiivinen' }}
+                  </span>
+                </td>
+                <td>{{ formatDate(user.last_login) }}</td>
+                <td>
+                  <div class="btn-group" role="group">
+                    <button 
+                      class="btn btn-sm btn-outline-primary" 
+                      @click="editUser(user)"
+                      title="Muokkaa"
+                    >
+                      <i class="fas fa-edit"></i>
+                    </button>
+                    <button 
+                      class="btn btn-sm btn-outline-danger" 
+                      @click="deleteUser(user)"
+                      title="Poista"
+                      :disabled="currentUser && user.id === currentUser.id"
+                    >
+                      <i class="fas fa-trash"></i>
+                    </button>
+                  </div>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+
+    <!-- Create/Edit User Modal -->
+    <div class="modal fade" :class="{ show: showCreateModal || showEditModal }" 
+         :style="{ display: showCreateModal || showEditModal ? 'block' : 'none' }">
+      <div class="modal-dialog">
+        <div class="modal-content">
+          <div class="modal-header">
+            <h5 class="modal-title">
+              {{ editingUser ? 'Muokkaa käyttäjää' : 'Luo uusi käyttäjä' }}
+            </h5>
+            <button type="button" class="btn-close" @click="closeModals"></button>
+          </div>
+          <div class="modal-body">
+            <form @submit.prevent="saveUser">
+              <div class="mb-3">
+                <label for="username" class="form-label">Käyttäjänimi</label>
+                <input 
+                  type="text" 
+                  class="form-control" 
+                  id="username" 
+                  v-model="formData.username" 
+                  required
+                  :disabled="editingUser"
+                >
+              </div>
+              
+              <div class="mb-3">
+                <label for="email" class="form-label">Sähköposti</label>
+                <input 
+                  type="email" 
+                  class="form-control" 
+                  id="email" 
+                  v-model="formData.email" 
+                  required
+                >
+              </div>
+              
+              <div class="row">
+                <div class="col-md-6 mb-3">
+                  <label for="firstName" class="form-label">Etunimi</label>
+                  <input 
+                    type="text" 
+                    class="form-control" 
+                    id="firstName" 
+                    v-model="formData.first_name" 
+                    required
+                  >
+                </div>
+                <div class="col-md-6 mb-3">
+                  <label for="lastName" class="form-label">Sukunimi</label>
+                  <input 
+                    type="text" 
+                    class="form-control" 
+                    id="lastName" 
+                    v-model="formData.last_name" 
+                    required
+                  >
+                </div>
+              </div>
+              
+              <div v-if="!editingUser" class="mb-3">
+                <label for="password" class="form-label">Salasana</label>
+                <input 
+                  type="password" 
+                  class="form-control" 
+                  id="password" 
+                  v-model="formData.password" 
+                  required
+                >
+              </div>
+              
+              <div v-else class="mb-3">
+                <label for="newPassword" class="form-label">Uusi salasana (jätä tyhjäksi jos et halua vaihtaa)</label>
+                <input 
+                  type="password" 
+                  class="form-control" 
+                  id="newPassword" 
+                  v-model="formData.password"
+                  placeholder="Jätä tyhjäksi jos et halua vaihtaa salasanaa"
+                >
+              </div>
+              
+              <div class="row">
+                <div class="col-md-6 mb-3">
+                  <label for="role" class="form-label">Rooli</label>
+                  <select class="form-select" id="role" v-model="formData.role">
+                    <option value="user">Käyttäjä</option>
+                    <option value="admin">Ylläpitäjä</option>
+                  </select>
+                </div>
+                <div class="col-md-6 mb-3">
+                  <label for="isActive" class="form-label">Tila</label>
+                  <select class="form-select" id="isActive" v-model="formData.is_active">
+                    <option :value="true">Aktiivinen</option>
+                    <option :value="false">Passiivinen</option>
+                  </select>
+                </div>
+              </div>
+              
+              <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" @click="closeModals">
+                  Peruuta
+                </button>
+                <button type="submit" class="btn btn-primary">
+                  {{ editingUser ? 'Tallenna' : 'Luo' }}
+                </button>
+              </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import api from '../api/axios'
+
+export default {
+  name: 'UserManagement',
+  props: {
+    currentUser: {
+      type: Object,
+      default: null
+    }
+  },
+  data() {
+    return {
+      users: [],
+      loading: false,
+      showCreateModal: false,
+      showEditModal: false,
+      editingUser: null,
+      formData: {
+        username: '',
+        email: '',
+        first_name: '',
+        last_name: '',
+        password: '',
+        role: 'user',
+        is_active: true
+      }
+    }
+  },
+  
+  async mounted() {
+    await this.loadUsers()
+  },
+  
+  methods: {
+        
+    async loadUsers() {
+      this.loading = true
+      try {
+        const response = await api.get('/users.php')
+        this.users = response.data.records || []
+      } catch (error) {
+        console.error('Error loading users:', error)
+        this.$emit('error', 'Käyttäjien lataaminen epäonnistui')
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    editUser(user) {
+      this.editingUser = user
+      this.formData = {
+        id: user.id,
+        username: user.username,
+        email: user.email,
+        first_name: user.first_name,
+        last_name: user.last_name,
+        password: '',
+        role: user.role,
+        is_active: user.is_active === '1' || user.is_active === 1 || user.is_active === true
+      }
+      this.showEditModal = true
+    },
+    
+    async saveUser() {
+      try {
+        if (this.editingUser) {
+          await api.put('/users.php', this.formData)
+          this.$emit('success', 'Käyttäjä päivitetty onnistuneesti')
+        } else {
+          await api.post('/users.php', this.formData)
+          this.$emit('success', 'Käyttäjä luotu onnistuneesti')
+        }
+        
+        this.closeModals()
+        await this.loadUsers()
+      } catch (error) {
+        console.error('Error saving user:', error)
+        const message = error.response?.data?.message || 'Käyttäjän tallentaminen epäonnistui'
+        this.$emit('error', message)
+      }
+    },
+    
+    async deleteUser(user) {
+      if (!confirm(`Oletko varma että haluat poistaa käyttäjän ${user.username}?`)) {
+        return
+      }
+      
+      try {
+        await api.delete(`/users?id=${user.id}`)
+        this.$emit('success', 'Käyttäjä poistettu onnistuneesti')
+        await this.loadUsers()
+      } catch (error) {
+        console.error('Error deleting user:', error)
+        const message = error.response?.data?.message || 'Käyttäjän poistaminen epäonnistui'
+        this.$emit('error', message)
+      }
+    },
+    
+    closeModals() {
+      this.showCreateModal = false
+      this.showEditModal = false
+      this.editingUser = null
+      this.formData = {
+        username: '',
+        email: '',
+        first_name: '',
+        last_name: '',
+        password: '',
+        role: 'user',
+        is_active: true
+      }
+    },
+    
+    getRoleBadgeClass(role) {
+      return role === 'admin' ? 'badge bg-danger' : 'badge bg-primary'
+    },
+    
+    getRoleLabel(role) {
+      return role === 'admin' ? 'Ylläpitäjä' : 'Käyttäjä'
+    },
+    
+    getStatusBadgeClass(isActive) {
+      return isActive ? 'badge bg-success' : 'badge bg-secondary'
+    },
+    
+    formatDate(dateString) {
+      if (!dateString) return 'Ei koskaan'
+      return new Date(dateString).toLocaleString('fi-FI')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.user-management {
+  padding: 20px;
+}
+
+.modal {
+  background-color: rgba(0, 0, 0, 0.5);
+}
+
+.modal.show {
+  display: block !important;
+}
+
+.modal-header {
+  background-color: #f8f9fa;
+  border-bottom: 1px solid #dee2e6;
+  color: #495057;
+}
+
+.modal-title {
+  color: #495057 !important;
+}
+
+.modal-body {
+  background-color: #ffffff;
+  color: #333;
+}
+
+.modal-footer {
+  background-color: #f8f9fa;
+  border-top: 1px solid #dee2e6;
+}
+
+/* Form styling for modal */
+.form-label {
+  color: #495057;
+  font-weight: 500;
+}
+
+.form-control, .form-select {
+  color: #495057;
+  background-color: #ffffff;
+  border-color: #ced4da;
+}
+
+.form-control:focus, .form-select:focus {
+  color: #495057;
+  background-color: #ffffff;
+  border-color: #80bdff;
+  box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
+}
+
+.form-check-input {
+  background-color: #ffffff;
+  border-color: #ced4da;
+}
+
+.form-check-input:checked {
+  background-color: #007bff;
+  border-color: #007bff;
+}
+
+.form-check-input:focus {
+  border-color: #80bdff;
+  box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
+}
+
+.form-check-label {
+  color: #495057;
+  margin-left: 8px;
+}
+
+.btn-group .btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+/* Fix white-on-white issue in table */
+.table {
+  color: #333 !important;
+}
+
+.table th {
+  background-color: #f8f9fa;
+  color: #495057;
+  border-bottom: 2px solid #dee2e6;
+}
+
+.table td {
+  color: #333;
+  border-top: 1px solid #dee2e6;
+}
+
+.table-striped tbody tr:nth-of-type(odd) {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+.table-striped tbody tr:nth-of-type(even) {
+  background-color: #ffffff;
+}
+
+.table tbody tr:hover {
+  background-color: #f8f9fa;
+}
+
+/* Ensure badges are visible with proper contrast */
+.badge {
+  color: white !important;
+  font-weight: 500;
+  padding: 4px 8px;
+  border-radius: 4px;
+  font-size: 0.75em;
+}
+
+/* Role badge specific styling */
+.badge.bg-danger {
+  background-color: #dc3545 !important;
+}
+
+.badge.bg-primary {
+  background-color: #007bff !important;
+}
+
+.badge.bg-success {
+  background-color: #28a745 !important;
+}
+
+.badge.bg-secondary {
+  background-color: #6c757d !important;
+}
+
+/* Status badge specific styling */
+.badge.bg-warning {
+  background-color: #ffc107 !important;
+  color: #212529 !important;
+}
+
+.badge.bg-info {
+  background-color: #17a2b8 !important;
+}
+
+/* Fix button text color and visibility */
+.btn-outline-primary {
+  color: #007bff !important;
+  border-color: #007bff;
+  background-color: #007bff;
+  padding: 6px 12px;
+  min-width: 36px;
+  min-height: 36px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.btn-outline-primary:hover {
+  background-color: #0056b3;
+  color: white !important;
+  border-color: #0056b3;
+}
+
+.btn-outline-primary:focus {
+  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
+}
+
+.btn-outline-danger {
+  color: white !important;
+  border-color: #dc3545;
+  background-color: #dc3545;
+  padding: 6px 12px;
+  min-width: 36px;
+  min-height: 36px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.btn-outline-danger:hover {
+  background-color: #c82333;
+  color: white !important;
+  border-color: #c82333;
+}
+
+.btn-outline-danger:focus {
+  box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
+}
+
+.btn-outline-danger:disabled {
+  color: #6c757d !important;
+  border-color: #6c757d;
+  background-color: transparent;
+  opacity: 0.6;
+}
+
+/* Ensure icons are visible */
+.btn i {
+  font-size: 14px;
+  line-height: 1;
+}
+
+/* Button group styling */
+.btn-group .btn {
+  margin-right: 4px;
+}
+
+.btn-group .btn:last-child {
+  margin-right: 0;
+}
+</style>

+ 4 - 3
frontend/src/components/accounting/ALVLaskentaSection.vue

@@ -200,7 +200,7 @@
 </template>
 
 <script>
-import axios from '../../api/axios'
+import api from '../../api/axios'
 import { formatCurrency, formatDate } from '../../utils/locale'
 
 export default {
@@ -309,10 +309,11 @@ export default {
         // Load company details
         await this.loadCompanyDetails()
         
-        const response = await axios.get('/api/alv_laskenta.php', {
+        const response = await api.get('/alv_laskenta.php', {
           params: {
             period: this.selectedPeriod,
             month: this.selectedMonth,
+            quarter: this.selectedQuarter,
             year: this.selectedYear
           }
         })
@@ -332,7 +333,7 @@ export default {
     
     async loadCompanyDetails() {
       try {
-        const response = await axios.get('/api/company.php')
+        const response = await api.get('/company.php')
         if (response.data.success) {
           const companyData = response.data.data
           this.companyName = companyData.name

+ 6 - 6
frontend/src/components/accounting/ExcelAccountingSection.vue

@@ -247,7 +247,7 @@
 </template>
 
 <script>
-import axios from '../../api/axios'
+import api from '../../api/axios'
 import { formatCurrency, formatDate } from '../../utils/locale'
 
 export default {
@@ -340,7 +340,7 @@ export default {
     async fetchEntries() {
       this.loading = true
       try {
-        const response = await axios.get('/api/accounting_entries.php')
+        const response = await api.get('/accounting_entries.php')
         this.entries = response.data.records || []
       } catch (error) {
         console.error('Error fetching entries:', error)
@@ -351,7 +351,7 @@ export default {
     async fetchCategories() {
       this.categoriesLoading = true
       try {
-        const response = await axios.get('/api/accounting_categories.php')
+        const response = await api.get('/accounting_categories.php')
         this.categories = response.data || []
       } catch (error) {
         console.error('Error fetching categories:', error)
@@ -485,9 +485,9 @@ export default {
     async saveEntry() {
       try {
         if (this.isEditingEntry) {
-          await axios.put(`/api/accounting_entries.php?id=${this.entryForm.id}`, this.entryForm)
+          await api.put(`/accounting_entries.php?id=${this.entryForm.id}`, this.entryForm)
         } else {
-          await axios.post('/api/accounting_entries.php', this.entryForm)
+          await api.post('/accounting_entries.php', this.entryForm)
         }
         
         this.closeEntryModal()
@@ -502,7 +502,7 @@ export default {
       }
       
       try {
-        await axios.delete(`/api/accounting_entries.php?id=${id}`)
+        await api.delete(`/accounting_entries.php?id=${id}`)
         this.fetchEntries()
       } catch (error) {
         console.error('Error deleting entry:', error)

+ 4 - 3
frontend/src/components/accounting/TuloslaskelmaSection.vue

@@ -319,7 +319,7 @@
 </template>
 
 <script>
-import axios from '../../api/axios'
+import api from '../../api/axios'
 import { formatCurrency, formatDate } from '../../utils/locale'
 
 export default {
@@ -460,10 +460,11 @@ export default {
         // Load company details
         await this.loadCompanyDetails()
         
-        const response = await axios.get('/api/tuloslaskelma.php', {
+        const response = await api.get('/tuloslaskelma.php', {
           params: {
             period: this.selectedPeriod,
             month: this.selectedMonth,
+            quarter: this.selectedQuarter,
             year: this.selectedYear
           }
         })
@@ -483,7 +484,7 @@ export default {
     
     async loadCompanyDetails() {
       try {
-        const response = await axios.get('/api/company.php')
+        const response = await api.get('/company.php')
         if (response.data.success) {
           const companyData = response.data.data
           this.companyName = companyData.name

+ 62 - 2
frontend/src/components/common/NavigationTabs.vue

@@ -59,10 +59,36 @@
       </div>
     </div>
     
+    <!-- User Management (Admin Only) -->
+    <button 
+      v-if="currentUser && currentUser.role === 'admin'"
+      :class="['nav-tab', { active: activeSection === 'user-management' }]" 
+      @click="$emit('section-change', 'user-management')"
+    >
+      Käyttäjät
+    </button>
+    
+    <!-- Profile Management (All Users) -->
+    <button 
+      v-if="currentUser"
+      :class="['nav-tab', { active: activeSection === 'profile' }]" 
+      @click="$emit('section-change', 'profile')"
+    >
+      Profiili
+    </button>
+    
     <!-- Timer Component -->
     <div class="timer-section">
       <HeaderTimer @navigate-to-tasks="$emit('section-change', 'tasks')" />
     </div>
+    
+    <!-- Logout Button -->
+    <div class="logout-section" v-if="currentUser">
+      <button class="logout-btn" @click="$emit('logout')">
+        <i class="fas fa-sign-out-alt"></i>
+        Kirjaudu ulos
+      </button>
+    </div>
   </div>
 </template>
 
@@ -78,10 +104,14 @@ export default {
     activeSection: {
       type: String,
       required: true,
-      validator: (value) => ['items', 'clients', 'projects', 'accounting', 'tuloslaskelma', 'alv-laskenta', 'tasks'].includes(value)
+      validator: (value) => ['items', 'clients', 'projects', 'accounting', 'tuloslaskelma', 'alv-laskenta', 'tasks', 'user-management', 'profile'].includes(value)
+    },
+    currentUser: {
+      type: Object,
+      default: null
     }
   },
-  emits: ['section-change'],
+  emits: ['section-change', 'logout'],
   data() {
     return {
       dropdownOpen: false
@@ -218,4 +248,34 @@ export default {
 .dropdown-item:last-child {
   border-radius: 0 0 4px 4px;
 }
+
+.logout-section {
+  display: flex;
+  align-items: center;
+  margin-left: auto;
+  padding-left: 20px;
+}
+
+.logout-btn {
+  background: #dc3545;
+  color: white;
+  border: none;
+  padding: 8px 16px;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  transition: all 0.3s ease;
+}
+
+.logout-btn:hover {
+  background: #c82333;
+  transform: translateY(-1px);
+}
+
+.logout-btn i {
+  font-size: 16px;
+}
 </style>

+ 15 - 15
frontend/src/components/projects/TaskManagementSection.vue

@@ -326,7 +326,7 @@
 </template>
 
 <script>
-import axios from '../../api/axios'
+import api from '../../api/axios'
 
 export default {
   name: 'TaskManagementSection',
@@ -398,7 +398,7 @@ export default {
     async loadTasks() {
       this.loading = true
       try {
-        const response = await axios.get('/api/tasks.php')
+        const response = await api.get('/tasks.php')
         if (response.data.success) {
           this.tasks = response.data.data
           // Load work hours summary for each task
@@ -416,7 +416,7 @@ export default {
     async loadTasksWorkHours() {
       for (const task of this.tasks) {
         try {
-          const response = await axios.get(`/api/work_hours.php?task_id=${task.id}`)
+          const response = await api.get(`/work_hours.php?task_id=${task.id}`)
           if (response.data.success) {
             const workHours = response.data.data
             task.total_hours = workHours.reduce((sum, hour) => sum + parseFloat(hour.hours), 0)
@@ -432,7 +432,7 @@ export default {
     
     async loadActiveTimers() {
       try {
-        const response = await axios.get('/api/timers.php?action=active')
+        const response = await api.get('/timers.php?action=active')
         if (response.data.success) {
           this.activeTimers = response.data.data
         } else {
@@ -446,7 +446,7 @@ export default {
     
     async loadProjects() {
       try {
-        const response = await axios.get('/api/projects.php')
+        const response = await api.get('/projects.php')
         this.projects = response.data.records || []
       } catch (error) {
         console.error('Error loading projects:', error)
@@ -482,9 +482,9 @@ export default {
       try {
         let response
         if (this.isEditing) {
-          response = await axios.put(`/api/tasks.php?id=${this.taskForm.id}`, this.taskForm)
+          response = await api.put(`/tasks.php?id=${this.taskForm.id}`, this.taskForm)
         } else {
-          response = await axios.post('/api/tasks.php', this.taskForm)
+          response = await api.post('/tasks.php', this.taskForm)
         }
         
         if (response.data.success) {
@@ -501,7 +501,7 @@ export default {
     async deleteTask(taskId) {
       if (confirm('Haluatko varmasti poistaa tämän tehtävän?')) {
         try {
-          await axios.delete(`/api/tasks.php?id=${taskId}`)
+          await api.delete(`/api/tasks.php?id=${taskId}`)
           this.loadTasks()
         } catch (error) {
           console.error('Error deleting task:', error)
@@ -574,7 +574,7 @@ export default {
     
     async loadWorkHours(taskId) {
       try {
-        const response = await axios.get(`/api/work_hours.php?task_id=${taskId}`)
+        const response = await api.get(`/api/work_hours.php?task_id=${taskId}`)
         if (response.data.success) {
           this.workHours = response.data.data
           this.workHoursSummary = {
@@ -615,7 +615,7 @@ export default {
     
     async startTimer() {
       try {
-        const response = await axios.post('/api/timers.php', {
+        const response = await api.post('/timers.php', {
           action: 'start',
           task_id: this.selectedTask.id,
           user_id: 1, // Default to current user
@@ -635,7 +635,7 @@ export default {
     
     async stopTimer(timerId) {
       try {
-        const response = await axios.post('/api/timers.php', {
+        const response = await api.post('/timers.php', {
           action: 'stop',
           id: timerId
         })
@@ -708,12 +708,12 @@ export default {
       try {
         let response
         if (this.isEditingWorkHour) {
-          response = await axios.put(`/api/work_hours.php`, {
+          response = await api.put(`/work_hours.php`, {
             ...this.workHourForm,
             id: this.workHourForm.id
           })
         } else {
-          response = await axios.post('/api/work_hours.php', this.workHourForm)
+          response = await api.post('/work_hours.php', this.workHourForm)
         }
         
         if (response.data.success) {
@@ -729,7 +729,7 @@ export default {
     async deleteWorkHour(workHourId) {
       if (confirm('Haluatko varmasti poistaa tämän työtunnin?')) {
         try {
-          const response = await axios.delete(`/api/work_hours.php?id=${workHourId}`)
+          const response = await api.delete(`/api/work_hours.php?id=${workHourId}`)
           if (response.data.success) {
             this.loadWorkHours(this.selectedTask.id)
             this.loadTasks() // Refresh tasks to update total hours
@@ -742,7 +742,7 @@ export default {
     
     async loadTaskClientInfo(taskId) {
       try {
-        const response = await axios.get(`/api/work_hours.php?task_id=${taskId}`)
+        const response = await api.get(`/api/work_hours.php?task_id=${taskId}`)
         if (response.data.success && response.data.data.length > 0) {
           const firstWorkHour = response.data.data[0]
           this.selectedTask.client_hour_price = firstWorkHour.client_hour_price

+ 4 - 2
frontend/vite.config.js

@@ -7,8 +7,10 @@ export default defineConfig({
     port: 3000,
     proxy: {
       '/api': {
-        target: process.env.VUE_APP_API_URL?.replace(/\/$/, '') || 'http://localhost:8080',
-        changeOrigin: true
+        target: 'http://localhost:8080',
+        changeOrigin: true,
+        secure: false,
+        rewrite: (path) => path
       }
     }
   }