Laravel Vue.js POS Authentication

Laravel Vue.js POS – Part 3/7: Data Migration & Authentication

Introduction

Welcome to Part 3 of our Laravel Vue.js POS Tutorial Series! In this tutorial, we’ll handle data migration and implement essential user authentication features. You’ll create the necessary database tables and seed them with dummy data for roles and users. Then, we’ll build core authentication functionality—including login, logout, and changing system user passwords. These steps are crucial for securing your POS system and managing user access effectively.

In this step, you’ll set up a Pinia store to manage user login, logout, and session state within your Laravel Vue.js POS Authentication system. Pinia makes it easy to handle global authentication data securely and reactively across your Vue.js components.

Create Database Tables and Dummy Data for the Whole POS Project

To kick off Laravel Vue.js POS Authentication, use Laravel migrations and seeders to generate essential database tables like users, roles, and POS entities. Then, seed them with dummy data to simulate a working environment for testing authentication features.

Open the Command Prompt (CMD), navigate to your project root directory, and run the following command:

php artisan make:migration project_tables

Overwrite the generated migration file database/migrations/2025_07_02_061238_project_tables.php with this content

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        // update existing user table
        Schema::table('users', function (Blueprint $table) {
            $table->string('username')->unique();
            $table->enum('role', ['admin', 'cashier', 'superadmin']);
            $table->boolean('active');
            $table->bigInteger('created_by_id')->default(0);
            $table->bigInteger('updated_by_id')->default(0);
            $table->bigInteger('deleted_by_id')->default(0);
            $table->dropColumn('name');
            $table->dropColumn('email');
            $table->dropColumn('email_verified_at');
            $table->softDeletes();
        });
        // tables
        Schema::create('tables', function (Blueprint $table) {
            // Set the storage engine to InnoDB
            $table->engine = 'InnoDB';
            $table->id();
            $table->string('name', 50);
            $table->tinyInteger('status')->default(2);
            $table->string('invoice_no', 20)->nullable();
            $table->tinyInteger('discount')->default(0);
            $table->decimal('total_discount')->default(0);
            $table->decimal('grand_total')->default(0);
            $table->decimal('total')->default(0);
            $table->decimal('net_amount')->default(0);
            $table->integer('order')->default(1000);
            $table->bigInteger('created_by_id')->default(0);
            $table->bigInteger('updated_by_id')->default(0);
            $table->bigInteger('deleted_by_id')->default(0);
            $table->timestamps();
            $table->softDeletes();
        });

        // product
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name', 100);
            $table->foreignId('product_category_id');
            $table->decimal('unit_price');
            $table->string('image')->nullable();
            $table->integer('order')->default(1000);
            $table->bigInteger('created_by_id')->default(0);
            $table->bigInteger('updated_by_id')->default(0);
            $table->bigInteger('deleted_by_id')->default(0);
            $table->timestamps();
            $table->softDeletes();
        });

        // product category
        Schema::create('product_categories', function (Blueprint $table) {
            $table->id();
            $table->string('name', 50);
            $table->integer('order')->default(1000);
            $table->bigInteger('created_by_id')->default(0);
            $table->bigInteger('updated_by_id')->default(0);
            $table->bigInteger('deleted_by_id')->default(0);
            $table->timestamps();
            $table->softDeletes();
        });

        // order
        Schema::create('orders', function (Blueprint $table) {
            // Set the storage engine to InnoDB
            $table->engine = 'InnoDB';
            $table->id();
            $table->foreignId('table_id');
            $table->string('invoice_no', 20);
            $table->integer('discount')->default(0);
            $table->decimal('total_discount')->default(0);
            $table->decimal('grand_total')->default(0);
            $table->decimal('total')->default(0);
            $table->decimal('net_amount')->default(0);
            $table->decimal('receive_amount');
            $table->bigInteger('created_by_id')->default(0);
            $table->bigInteger('updated_by_id')->default(0);
            $table->bigInteger('deleted_by_id')->default(0);
            $table->timestamps();
            $table->softDeletes();
        });

        // order detail
        Schema::create('order_details', function (Blueprint $table) {
            // Set the storage engine to InnoDB
            $table->engine = 'InnoDB';
            $table->id();
            $table->foreignId('order_id');
            $table->foreignId('product_id');
            $table->foreignId('product_category_id');
            $table->string('description', 100);
            $table->integer('qty');
            $table->decimal('unit_price');
            $table->tinyInteger('discount')->default(0);
            $table->bigInteger('created_by_id')->default(0);
            $table->bigInteger('updated_by_id')->default(0);
            $table->bigInteger('deleted_by_id')->default(0);
            $table->timestamps();
            $table->softDeletes();
        });

        // order detail temp
        Schema::create('order_detail_temps', function (Blueprint $table) {
            // Set the storage engine to InnoDB
            $table->engine = 'InnoDB';
            $table->id();
            $table->foreignId('table_id');
            $table->foreignId('product_id');
            $table->foreignId('product_category_id');
            $table->string('description', 100);
            $table->integer('qty');
            $table->decimal('unit_price');
            $table->tinyInteger('discount')->default(0);
            $table->bigInteger('created_by_id')->default(0);
            $table->bigInteger('updated_by_id')->default(0);
            $table->timestamps();
        });

        // balance adjustment
        Schema::create('balance_adjustments', function (Blueprint $table) {
            $table->id();
            $table->decimal('amount');
            $table->tinyInteger('type_id');
            $table->date('adjustment_date');
            $table->string('remark');
            $table->bigInteger('created_by_id')->default(0);
            $table->bigInteger('updated_by_id')->default(0);
            $table->bigInteger('deleted_by_id')->default(0);
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tables');
        Schema::dropIfExists('products');
        Schema::dropIfExists('product_categories');
        Schema::dropIfExists('orders');
        Schema::dropIfExists('order_details');
        Schema::dropIfExists('order_detail_temps');
        Schema::dropIfExists('balance_adjustments');
    }
};

Overwrite the seeder file database/seeders/DatabaseSeeder.php with this content

<?php

namespace Database\Seeders;

use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // User::factory(10)->create();

        // User::factory()->create([
        //     'name' => 'Test User',
        //     'email' => 'test@example.com',
        // ]);

        DB::table('users')->insert([
            'username' => 'superadmin',
            'password' => bcrypt('123456'),
            'role' => 'superadmin',
            'active' => 1,
            'created_at' => date('Y-m-d H:i:s'),
            'updated_at' => date('Y-m-d H:i:s')
        ]);
        DB::table('users')->insert([
            'username' => 'admin',
            'password' => bcrypt('123456'),
            'role' => 'admin',
            'active' => 1,
            'created_at' => date('Y-m-d H:i:s'),
            'updated_at' => date('Y-m-d H:i:s')
        ]);
        DB::table('users')->insert([
            'username' => 'cashier',
            'password' => bcrypt('123456'),
            'role' => 'cashier',
            'active' => 1,
            'created_at' => date('Y-m-d H:i:s'),
            'updated_at' => date('Y-m-d H:i:s')
        ]);

        // insert dummy product category
        $dummyProducts = [
            'Rice' => [
                [
                    'name' => 'Pork Rice',
                    'unit_price' => 2
                ],
                [
                    'name' => 'Chicken Rice',
                    'unit_price' => 2
                ],
                [
                    'name' => 'Fried Rice',
                    'unit_price' => 2
                ]
            ],
            'Noodle' => [
                [
                    'name' => 'Beef Noodle Soup',
                    'unit_price' => 3
                ],
                [
                    'name' => 'Seafood Noodle Soup',
                    'unit_price' => 3
                ]
            ],
            'Snack' => [
                [
                    'name' => 'Fried Chicken',
                    'unit_price' => 1
                ],
                [
                    'name' => 'French Fries',
                    'unit_price' => 1
                ],
                [
                    'name' => 'Burger',
                    'unit_price' => 1
                ]
            ],
            'Water' => [
                [
                    'name' => 'Eau Kulen',
                    'unit_price' => 0.25
                ],
                [
                    'name' => 'Angkor Puro',
                    'unit_price' => 0.25
                ],
                [
                    'name' => 'Dasani',
                    'unit_price' => 0.25
                ]
            ],
            'Coffee' => [
                [
                    'name' => 'Iced Latte',
                    'unit_price' => 1.5
                ],
                [
                    'name' => 'Iced Cappuccino',
                    'unit_price' => 1.5
                ],
                [
                    'name' => 'Iced Americano',
                    'unit_price' => 1.5
                ]
            ],
            'Beer & Wine' => [
                [
                    'name' => 'ABC',
                    'unit_price' => 2
                ],
                [
                    'name' => 'Tiger',
                    'unit_price' => 2
                ],
                [
                    'name' => 'Heineken',
                    'unit_price' => 2
                ]
            ],
            'Juice' => [
                [
                    'name' => 'Orange Juice',
                    'unit_price' => 2
                ],
                [
                    'name' => 'Apple Juice',
                    'unit_price' => 2
                ],
                [
                    'name' => 'Mango Juice',
                    'unit_price' => 2
                ]
            ],
        ];

        foreach ($dummyProducts as $key => $value) {
            $categoryId = DB::table('product_categories')->insertGetId([
                'name' => $key,
                'created_at' => date('Y-m-d H:i:s'),
                'updated_at' => date('Y-m-d H:i:s')
            ]);
            foreach ($value as $product) {
                DB::table('products')->insert([
                    'name' => $product['name'],
                    'unit_price' => $product['unit_price'],
                    'product_category_id' => $categoryId,
                    'created_at' => date('Y-m-d H:i:s'),
                    'updated_at' => date('Y-m-d H:i:s')
                ]);
            }
        }

        // Dummy Table
        for ($i = 1; $i <= 10; $i++) {
            DB::table('tables')->insert([
                'name' => str_pad($i, 3, '0', STR_PAD_LEFT),
                'created_at' => date('Y-m-d H:i:s'),
                'updated_at' => date('Y-m-d H:i:s')
            ]);
        }
    }
}

Then run the migration:

php artisan migrate:fresh --seed

Note: After running this command, Laravel will generate all the required project tables along with sample data for products and their categories. Additionally, three default user accounts will be created for testing:

  • Username: superadmin | Password: 123456
  • Username: admin         | Password: 123456
  • Username: cashier      | Password: 123456

These users can be used to log in and test different access levels within the system.

To learn more about Laravel’s migration system, you can check out the official Laravel migration documentation.

Setup Controller for Laravel API

In this step of building Laravel Vue.js POS Authentication, you’ll create backend API controllers to handle user login, logout, password changes, and profile management. These APIs will securely power your frontend authentication system.

Replace the contents of app/Http/Controllers/AuthController.php with the code below. If the file doesn’t exist yet, please create it.

<?php

namespace App\Http\Controllers;

use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class AuthController extends Controller
{
    public function login(Request $request)
    {
        // validation
        $rules = [
            'username' => 'required',
            'password' => 'required'
        ];
        $validator = Validator::make($request->all(), $rules);
        if ($validator->fails())
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ]);

        $credentials = [
            'username' => $request->username,
            'password' => $request->password,
            'active' => 1,
        ];

        if (Auth::attempt($credentials)) {
            return response()->json([
                'success' => true
            ]);
        }
        return response()->json([
            'success' => false,
            'errors' => [
                'username' => ['Incorrect username or password']
            ]
        ]);
    }

    public function user(Request $request)
    {
        $data = $request->user()->only('id', 'username', 'role');
        $data['server_time'] = date('d-M-Y H:i:s');
        return response()->json($data);
    }

    public function logout(Request $request)
    {
        Auth::logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();
        return response()->json(['success' => true]);
    }

    // Change password
    public function changePassword(Request $request)
    {
        $rules = [
            'old_password' => [
                'required',
                function ($attribute, $value, $fail) use ($request) {
                    if (!Hash::check($value, $request->user()->password)) {
                        $fail('The old password is incorrect');
                    }
                }
            ],
            'new_password' => ['required', 'confirmed'],
        ];
        //validate data
        $validator = Validator::make($request->all(), $rules);
        if ($validator->fails())
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ]);

        // change the password
        try {
            $user = $request->user();
            $user->updated_at = now();
            $user->updated_by_id = $user->id;
            $user->password = bcrypt($request->new_password);
            $user->save();

            $response['success'] = true;
            $response['data'] = null;
        } catch (Exception $ex) {
            abort($ex->getCode(), $ex->getMessage());
        }
        return response()->json($response);
    }
}

Create Share Component and Helper Function

Improve reusability and consistency in your Laravel Vue.js POS Authentication frontend by setting up shared Vue components (like input fields or alerts) and helper functions that streamline form handling and validation.

Replace the contents of resources/js/component/share/Modal.vue with the code below. If the file doesn’t exist yet, please create it.

<template>
    <div>
        <div class="modal fade" ref="modal" tabindex="-1" aria-hidden="true" data-bs-keyboard="false"
            data-bs-focus="false" style="z-index: 1055;">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div
                        :class="['modal-header fs-5 py-2', { 'text-bg-success': messageType == 1, 'text-bg-secondary': messageType == 2, 'text-bg-danger': messageType > 2 }]">
                        <i
                            :class="['bi', { 'bi-check-circle': messageType == 1, 'bi-exclamation-circle': messageType == 2, 'bi-x-circle': messageType > 2 }]"></i> {{
                                messageTitle }}
                    </div>
                    <div class="modal-body text-center fs-5">
                        {{ messageText }}
                    </div>
                    <div class="modal-footer justify-content-center">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
                            <i class="bi bi-x-lg"></i> NO
                        </button>
                        <button v-if="messageType == 4" class="btn btn-primary px-3" data-bs-dismiss="modal"
                            @click="deleteFunction">
                            <i class="bi bi-check-lg"></i> YES
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script setup>
import { Modal } from 'bootstrap';
import { onMounted, onUnmounted, ref } from 'vue';

const modal = ref(null);
const modalInstance = ref(null);

onMounted(() => {
    if (modal.value) {
        modalInstance.value = new Modal(modal.value);
        modal.value.addEventListener("hide.bs.modal", () => {
            document.activeElement?.blur();
        });
    }
});

onUnmounted(() => {
    if (modal) {
        modalInstance.value.dispose();
    }
});

const messageType = ref(1);
const messageTitle = ref(null);
const messageText = ref(null);
const deleteFunction = ref(null);

/**
 * Show greeting
 * @param type - type of modal (1: Success, 2: Info, 3: Error, 4: Confirm Delete)
 */
const showModal = (type, callback = null, title = null, message = null) => {
    messageType.value = type;
    if (type == 1) {
        messageTitle.value = "SUCCESS";
        messageText.value = "Your data has been saved successfully";
    } else if (type == 2) {
        messageTitle.value = "ALERT";
    }
    else if (type == 3) {
        messageTitle.value = "ERROR";
    }
    else if (type == 4) {
        messageTitle.value = "DELETE";
        messageText.value = "Are you sure want to delete?";
        deleteFunction.value = callback;
    }
    if (title)
        messageTitle.value = title;
    if (message)
        messageText.value = message;
    modalInstance.value?.show();
}

defineExpose({ showModal });

</script>

Replace the contents of resources/js/helper.js with the code below. If the file doesn’t exist yet, please create it.

import dayjs from "dayjs";
import { ref } from "vue";

// Number Format
export function numberFormat(amount, min_point = 2, max_point = 2) {
    const formatter = Intl.NumberFormat('en-US', {
        useGrouping: true,
        minimumFractionDigits: min_point,
        maximumFractionDigits: max_point
    });
    return formatter.format(Number(amount));
}

// Currency Format
export function currencyFormat(amount, min_point = 2, max_point = 2) {
    const formatter = Intl.NumberFormat('en-US', {
        useGrouping: true,
        minimumFractionDigits: min_point,
        maximumFractionDigits: max_point
    });
    return "$" + formatter.format(Number(amount));
}

// Date Format
export function dateFormat(date, excludeTime = false) {
    let format = "DD-MMM-YYYY HH:mm:ss";
    if (excludeTime)
        format = format.split(' ')[0];
    return dayjs(date).format(format);
}

// set focus
export function setFocus(input) {
    const autofocus = input.value;
    if (autofocus) {
        setTimeout(() => {
            autofocus.focus();
            autofocus.select();
        }, 5);
    }
};

// Clear reactive object
export function clearForm(obj) {
    for (const key in obj) {
        obj[key] = null;
    }
}

// date filter config
export const dateFilterConfig = ref({
    wrap: true,
    altFormat: "d-M-Y",
    altInput: true,
    dateFormat: "Y-m-d",
    enableTime: false,
    defaultHour: "00",
    time_24hr: true,
});

Replace the contents of resources/js/component/share/404.vue with the code below. If the file doesn’t exist, create it manually. This view will be used for the fallback route when no other routes match.

<template>
  <div class="d-flex align-items-center justify-content-center vh-100" style="background: whitesmoke">
    <div class="text-center">
      <h1 class="fw-bold" style="font-size: 200px">404</h1>
      <p class="fs-3"><span class="text-danger">Opps!</span> Page not found.</p>
      <p class="lead">The page you’re looking for doesn’t exist.</p>
      <router-link :to="{ path: '/' }" class="btn btn-primary pt-1">Go Home</router-link>
    </div>
  </div>
</template>
<script setup>
import { onMounted } from 'vue';

onMounted(() => {
  document.body.style.display = "block";
});
</script>

Setup Vue Component

Create Vue components specifically for your Laravel Vue.js POS Authentication pages, including login forms, password update screens, and dashboard layouts. This modular approach ensures clarity and maintainability.

Replace the contents of resources/js/component/layout/Admin.vue with the code below. If the file doesn’t exist, create it manually.

<template>
    <div>
        <!-- Include Modal Component-->
        <ShareModal ref="messageBox"></ShareModal>
        <!-- Form Modal -->
        <div class="modal fade" ref="formModal" tabindex="-1" aria-hidden="true" data-bs-keyboard="false"
            data-bs-backdrop="static" data-bs-focus="false">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header py-2 bg-secondary text-light">
                        <h5 class="modal-title" style="font-weight: bold">
                            Change Password
                        </h5>
                    </div>
                    <div class="modal-body">
                        <form @submit.prevent="changePassword" id="formPassword">
                            <div class="row">
                                <div class="col-12 mb-3">
                                    <label class="form-label">Name</label>
                                    <input type="text" class="form-control" :value="auth?.user?.username" disabled />
                                </div>
                                <div class="col-12 mb-3">
                                    <label class="form-label required">Old Password</label>
                                    <input type="password" :disabled="isLoading"
                                        :class="['form-control', { 'is-invalid': errors.old_password }]"
                                        v-model="form.old_password" ref="autofocus" />
                                    <span v-if="errors.old_password" class="invalid-feedback"> {{
                                        errors.old_password[0] }}
                                    </span>
                                </div>
                                <div class="col-12 mb-3">
                                    <label class="form-label required">New Password</label>
                                    <input type="password" :disabled="isLoading"
                                        :class="['form-control', { 'is-invalid': errors.new_password }]"
                                        v-model="form.new_password" />
                                    <span v-if="errors.new_password" class="invalid-feedback"> {{
                                        errors.new_password[0] }}
                                    </span>
                                </div>
                                <div class="col-12 mb-3">
                                    <label class="form-label required">New Passwo rd Confirmation</label>
                                    <input type="password" :disabled="isLoading"
                                        :class="['form-control', { 'is-invalid': errors.new_password_confirmation }]"
                                        v-model="form.new_password_confirmation" />
                                    <span v-if="errors.new_password_confirmation" class="invalid-feedback"> {{
                                        errors.new_password_confirmation }}
                                    </span>
                                </div>
                            </div>
                        </form>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
                            <i class="bi bi-x-lg"></i> Cancel
                        </button>
                        <button type="submit" class="btn btn-primary px-3" form="formPassword" :disabled="isLoading">
                            <i v-if="isLoading" class="spinner-border spinner-border-sm" role="status"
                                aria-hidden="true"></i>
                            <i v-else class="bi bi-floppy" style="padding-right: 3px;"></i> Save
                        </button>
                    </div>
                </div>
            </div>
        </div>

        <header id="header" class="header fixed-top d-flex align-items-center">
            <div class="d-flex align-items-center justify-content-between">
                <a href="/" class="logo d-flex align-items-center">
                    <img src="images/logo.png" alt="">
                </a>
                <i class="bi bi-list toggle-sidebar-btn"></i>
            </div>
            <nav class="header-nav ms-auto">
                <ul class="d-flex align-items-center">
                    <li class="d-none d-md-inline-block form-inline ms-auto nav-item dropdown me-5">
                        <i class="bi bi-alarm-fill text-secondary pe-2"></i>
                        <span class="text-secondary">{{ auth?.user?.server_time }}</span>
                    </li>
                    <li class="nav-item dropdown pe-3">
                        <a class="nav-link nav-profile d-flex align-items-center pe-0" href="#"
                            data-bs-toggle="dropdown">
                            <i class="bi bi-person-fill" style="font-size: 35px;"></i>
                            <span class="d-none d-md-block dropdown-toggle ps-2 text-capitalize">{{ auth?.user?.username
                                }}</span>
                        </a>
                        <ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow profile">
                            <li>
                                <button class="dropdown-item d-flex align-items-center" @click="openModal">
                                    <i class="bi bi-shield-lock"></i>
                                    <span>Change Password</span>
                                </button>
                            </li>
                            <li>
                                <hr class="dropdown-divider">
                            </li>
                            <li>
                                <button type="submit" class="dropdown-item d-flex align-items-center"
                                    @click="auth.logout()">
                                    <i class="bi bi-box-arrow-right"></i>
                                    <span>Sign Out</span>
                                </button>
                            </li>
                        </ul>
                    </li>
                </ul>
            </nav>

        </header>

        <!-- ======= Sidebar ======= -->
        <aside id="sidebar" class="sidebar">
            <ul class="sidebar-nav" id="sidebar-nav">
                <li class="nav-item">
                    <router-link :to="{ path: '/' }" :class="['nav-link', { collapsed: $route.path != '/' }]">
                        <i class="bi bi-speedometer2"></i>
                        <span>Dashboard</span>
                    </router-link>
                </li>
                <li class="nav-heading">Main Data</li>
                <li class="nav-item">
                    <router-link :to="{ path: 'table' }" :class="['nav-link', { collapsed: $route.path != 'table' }]">
                        <i class="bi bi-grid-3x3-gap"></i>
                        <span>Table</span>
                    </router-link>
                </li>
                <li class="nav-item">
                    <router-link :to="{ path: 'product' }"
                        :class="['nav-link', { collapsed: $route.path != 'product' }]">
                        <i class="bi bi-list-ul"></i>
                        <span>Product</span>
                    </router-link>
                </li>
                <li class="nav-item">
                    <router-link :to="{ path: 'product-category' }"
                        :class="['nav-link', { collapsed: $route.path != 'product-category' }]">
                        <i class="bi bi-grid"></i>
                        <span>Product Category</span>
                    </router-link>
                </li>
                <li class="nav-heading">Operation</li>
                <li class="nav-item">
                    <router-link :to="{ path: 'balance-adjustment' }"
                        :class="['nav-link', { collapsed: $route.path != 'balance-adjustment' }]">
                        <i class="bi bi-coin"></i>
                        <span>Balance Adjustment</span>
                    </router-link>
                </li>
                <li class="nav-heading">Report</li>
                <li class="nav-item">
                    <router-link :to="{ path: 'sale-summary' }"
                        :class="['nav-link', { collapsed: $route.path != 'sale-summary' }]">
                        <i class="bi bi-graph-up-arrow"></i>
                        <span>Sale Summary</span>
                    </router-link>
                </li>
                <li class="nav-item">
                    <router-link :to="{ path: 'product-summary' }"
                        :class="['nav-link', { collapsed: $route.path != 'product-summary' }]">
                        <i class="bi bi-clipboard-data"></i>
                        <span>Product Summary</span>
                    </router-link>
                </li>
                <li class="nav-item">
                    <router-link :to="{ path: 'sale-history' }"
                        :class="['nav-link', { collapsed: $route.path != 'sale-history' }]">
                        <i class="bi bi-clock-history"></i>
                        <span>Sale History</span>
                    </router-link>
                </li>
                <li class="nav-heading" v-if="auth.user?.role == 'superadmin'">System Setting</li>
                <li class="nav-item" v-if="auth.user?.role == 'superadmin'">
                    <router-link :to="{ path: 'user' }" :class="['nav-link', { collapsed: $route.path != 'user' }]">
                        <i class="bi bi-people"></i>
                        <span>System User</span>
                    </router-link>
                </li>
            </ul>
        </aside>

        <main id="main" class="main">
            <router-view></router-view>
        </main>
    </div>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { Modal } from 'bootstrap';
import { useAuthStore } from '@/store/auth';
import ShareModal from '../Share/Modal.vue';
import { clearForm, setFocus } from '../../helper.js';

const formModalInstance = ref(null);
const formModal = ref(null);
const autofocus = ref(null);
const messageBox = ref(null);
const isLoading = ref(false);
const auth = useAuthStore();

const form = ref(
    {
        old_password: null,
        new_password: null,
        new_password_confirmation: null
    }
);
const errors = ref({});

onMounted(() => {
    import("../../main.js");
    document.body.style.display = "block";

    if (formModal.value) {
        formModalInstance.value = new Modal(formModal.value);
        formModal.value.addEventListener("shown.bs.modal", () => {
            setFocus(autofocus);
        });
        formModal.value.addEventListener("hide.bs.modal", () => {
            document.activeElement?.blur();
        });
        formModal.value.addEventListener("hidden.bs.modal", () => {
            clearForm(form.value);
            errors.value = {};
        });
    }
});
onUnmounted(() => {
    if (formModalInstance.value) {
        formModalInstance.value.dispose();
    }
});

// add or create
const openModal = () => {
    formModalInstance.value.show();
};

// submit form
const changePassword = () => {
    isLoading.value = true;
    axios.post("api/auth/change-password", form.value)
        .then((response) => {
            if (response.data.success) {
                formModalInstance.value.hide();
                messageBox.value.showModal(1, null, null, 'Your password has been changed successfully');
            } else {
                errors.value = response.data.errors;
                setFocus(autofocus);
            }
        })
        .catch((ex) => {
            console.log(ex);
            setFocus(autofocus);
        })
        .finally(() => {
            isLoading.value = false;
        });
};
</script>

Create Pinia Store for Login and User Session Management

Manage global authentication state in your Laravel Vue.js POS Authentication system using Pinia. This includes storing the user token, login status, and profile data for secure and reactive state handling.

Replace the contents of resources/js/store/auth.js with the code below. If the file doesn’t exist, create it manually.

import { defineStore } from 'pinia'
import router from '../router'

export const useAuthStore = defineStore('auth', {
    state: () => ({
        user: null,
    }),

    actions: {
        async getUser() {
            try {
                const response = await axios.get('/api/auth/user')
                this.user = response.data
            } catch {
                this.user = null
            }
        },
        logout() {
            axios.post('/api/logout').then(() => { this.user = null; router.push('/login'); });
        }
    }
})

Setup Vue Router

Configure Vue Router to handle navigation in your Laravel Vue.js POS Authentication system. Define routes for login, dashboard, and error pages while implementing navigation guards to protect authenticated views.

Replace the contents of resources/js/router/index.js with the code below. If the file doesn’t exist, create it manually.

import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/store/auth';

const routes = [
    {
        name: "login",
        path: "/login",
        component: () => import("@/component/share/Login.vue"),
    },
    {
        path: "/",
        component: () => import("@/component/layout/Admin.vue"),
        meta: {
            requiresAuth: true
        },
        children: [
            // ========= Dashboard =========
            {
                path: "/",
                component: () => import("@/component/dashboard/Dashboard.vue"),
            },
        ],
    },
    {
        path: "/:pathMatch(.*)*",
        component: () => import("@/component/share/404.vue"),
    }
]

const router = createRouter({
    history: createWebHistory(),
    routes,
})
router.beforeEach(async (to, from, next) => {
    const auth = useAuthStore()
    if (auth.user === null) {
        await auth.getUser();
    }
    if (to.meta.requiresAuth && !auth.user) {
        next('/login')
    } else {
        next()
    }
})
export default router

Setup Route for Backend

Register the necessary API routes in routes/api.php to support Laravel Vue.js POS Authentication. These routes will handle user login, logout, and password updates, all protected with authentication middleware.

Replace the contents of routes/web.php with the code below

<?php

use Illuminate\Support\Facades\Route;

Route::get('{any}', function () {
    return view('welcome');
})->where('any', '.*');

Replace the contents of routes/api.php with the code below

<?php

use App\Http\Controllers\AuthController;
use Illuminate\Support\Facades\Route;

Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout']);

Route::middleware('auth:sanctum')->group(function () {
    // Auth
    Route::prefix('auth')->controller(AuthController::class)->group(function () {
        Route::get('/user',  'user');
        Route::post('/change-password', [AuthController::class, 'changePassword']);
    });
});

Compile Assets

Finalize your Laravel Vue.js POS Authentication setup by compiling Vue components, JavaScript, and styles using Vite or Laravel Mix. This step ensures your assets are production-ready and your authentication interface loads smoothly.

Run the following command to compile your assets:

npm run dev

Use <strong>npm run build</strong> for production.

Once compiled, visit:

http://laravel-vuejs-pos

You should now see your Laravel Vue.js POS project running with the database tables, sample data, and a basic login system ready for upcoming features.

Please use this default account to log in

  • Username: superadmin | Password: 123456
  • Username: admin         | Password: 123456
  • Username: cashier      | Password: 123456

Setup Complete

Well done! 🎉 You’ve now completed the foundational setup of your Laravel Vue.js POS project. In this part, you created essential database tables, added sample data, and implemented a basic authentication system—giving your POS app real structure and secure access control.

In the next tutorial, we’ll move on to building CRUD functionality for managing products, categories, tables, users, and balance adjustment. You’ll learn how to create, edit, and delete records using Laravel’s powerful tools—bringing your POS system to life.

👉 Continue to Part 4: Navigation & CRUD Operations

Laravel Vue.js POS Tutorial for Beginners Series

This step-by-step series will guide you through building a complete Laravel Vue.js POS system from scratch:

Leave a Reply

Your email address will not be published. Required fields are marked *