Merge pull request 'auth front and back' (#2) from feature/auth into main

Reviewed-on: #2
This commit is contained in:
Kevin 2025-10-06 17:39:19 +02:00
commit 8f06d79f42
54 changed files with 2408 additions and 597 deletions

View File

@ -6,11 +6,12 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.

View File

@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)

View File

@ -11,6 +11,7 @@
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.2",
"laravel/tinker": "^2.10.1"
},
"require-dev": {
@ -75,4 +76,4 @@
},
"minimum-stability": "stable",
"prefer-stable": true
}
}

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c514d8f7b9fc5970bdd94287905ef584",
"content-hash": "8f387a0734f3bf879214e4aa2fca6e2f",
"packages": [
{
"name": "brick/math",
@ -1332,6 +1332,70 @@
},
"time": "2025-09-19T13:47:56+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0",
"illuminate/contracts": "^11.0|^12.0",
"illuminate/database": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"php": "^8.2",
"symfony/console": "^7.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0|^10.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^11.3"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2025-07-09T19:45:24+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.5",

View File

@ -0,0 +1,28 @@
<?php
return [
// Apply CORS to API routes and Sanctum's CSRF cookie endpoint (if used)
'paths' => ['api/*', 'sanctum/csrf-cookie'],
// Allow all HTTP methods for simplicity in dev
'allowed_methods' => ['*'],
// IMPORTANT: Do NOT use '*' when sending credentials. List explicit origins.
// Set FRONTEND_URL in .env to override the default if needed.
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:8080')],
// Alternatively, use patterns (kept empty for clarity)
'allowed_origins_patterns' => [],
// Headers the client may send
'allowed_headers' => ['*'],
// Headers exposed to the browser
'exposed_headers' => [],
// Preflight cache duration (in seconds)
'max_age' => 0,
// Must be true if the browser sends cookies or Authorization with withCredentials
'supports_credentials' => true,
];

View File

@ -0,0 +1,33 @@
<?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
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@ -19,6 +19,8 @@ Route::prefix('auth')->group(function () {
Route::middleware('auth:sanctum')->group(function () {
Route::get('/me', [AuthController::class, 'me']);
// Alias to support clients calling /api/auth/user
Route::get('/user', [AuthController::class, 'me']);
Route::post('/logout', [AuthController::class, 'logout']);
Route::post('/logout-all', [AuthController::class, 'logoutAll']);
});

View File

@ -1 +1,3 @@
NODE_ENV=
# API base URL for axios (used by src/services/http.ts)
# For Laravel Sanctum on local dev (default Laravel port):
VUE_APP_API_BASE_URL=http://localhost:8000

View File

@ -1,3 +0,0 @@
dist/
node_modules/
public/

View File

@ -1,19 +1,47 @@
module.exports = {
root: true,
env: {
node: true,
browser: true,
node: true,
es6: true,
},
extends: [
'plugin:vue/vue3-essential',
'@vue/prettier',
"eslint:recommended",
"plugin:vue/vue3-recommended",
"@vue/typescript",
"@vue/prettier",
],
parserOptions: {
parser: 'babel-eslint',
parser: "@typescript-eslint/parser",
ecmaVersion: 2020,
sourceType: "module",
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// Relax console/debugger in development
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
ignorePatterns: [
"node_modules/",
"dist/",
"public/*.min.js",
"public/**/*.min.js",
],
// Disable base no-unused-vars in .vue files to avoid false positives with <script setup>
overrides: [
{
files: ["*.vue"],
rules: {
"no-unused-vars": "off",
},
},
],
};

View File

@ -0,0 +1,167 @@
# Authentication Setup - Token Persistence
## ✅ Fixed: Token persists across page refreshes
### The Problem
After login, refreshing the page redirected to `/login` because the auth state wasn't being restored from the saved token.
### The Solution
**No plugin needed!** The token is already saved in localStorage. I updated the router guard to check for the token on page load.
---
## How it works
### 1. **Login Flow**
```
User submits login form
POST /api/auth/login
Backend returns: { success: true, data: { user, token } }
Token saved to localStorage.setItem("auth_token", token)
User saved to Pinia store
Redirect to dashboard
```
### 2. **Page Refresh Flow**
```
User refreshes page
Router guard runs (beforeEach)
Check: auth.checked? No
Check: localStorage.getItem("auth_token")? Yes
Set auth.token = token (from localStorage)
Fetch user: GET /api/auth/user (with Bearer token)
Backend returns user data
auth.user = userData
auth.checked = true
User stays on protected route
```
### 3. **Logout Flow**
```
User clicks logout
POST /api/auth/logout
localStorage.removeItem("auth_token")
auth.user = null
auth.token = null
Redirect to /login
```
---
## Files Modified
### `src/services/auth.ts`
- `login()` saves token to localStorage
- `logout()` removes token from localStorage
- `me()` fetches user from `/api/auth/user`
- `getToken()` retrieves token from localStorage
### `src/services/http.ts`
- Automatically adds `Authorization: Bearer {token}` header to all requests
- Reads token from localStorage on every request
### `src/stores/auth.ts`
- Stores both `user` and `token` in state
- `isAuthenticated` checks for both user AND token
- `login()` extracts user and token from API response
### `src/router/index.js`
- On every navigation, checks if token exists in localStorage
- If token exists and auth not checked yet:
1. Sets `auth.token = token`
2. Fetches user data with `auth.fetchMe()`
3. If successful, user stays authenticated
4. If fails (invalid token), clears token and redirects to login
### `src/views/pages/Login.vue`
- Form submits to `authStore.login()`
- Shows loading state and error messages
- Redirects to dashboard on success
---
## API Endpoints Used
| Endpoint | Method | Purpose | Auth Required |
|----------|--------|---------|---------------|
| `/api/auth/login` | POST | Login with email/password | No |
| `/api/auth/user` | GET | Get current user | Yes (Bearer token) |
| `/api/auth/logout` | POST | Logout | Yes (Bearer token) |
---
## Testing
### Test 1: Fresh Login
1. Go to `/login`
2. Enter credentials: `admin@admin.com`
3. Click "Se connecter"
4. Should redirect to `/dashboards/dashboard-default`
5. Check localStorage: `auth_token` should be present
### Test 2: Page Refresh
1. After login, refresh the page
2. Should stay on dashboard (not redirect to login)
3. Check console: should see "Fetching user from /api/auth/user"
### Test 3: Invalid Token
1. Manually edit token in localStorage to garbage value
2. Refresh page
3. Should clear token and redirect to login
4. Check console: "Invalid token, clearing auth"
### Test 4: Logout
1. After login, click logout
2. Should redirect to `/login`
3. Check localStorage: `auth_token` should be removed
---
## Troubleshooting
### "Still redirects to /login after refresh"
- Open browser DevTools → Network tab
- Refresh page
- Check if `GET /api/auth/user` is called with `Authorization: Bearer ...` header
- Check response:
- **200 OK**: Token is valid, check if user data is correct format
- **401 Unauthorized**: Token is invalid or backend isn't verifying correctly
### "Token is saved but still logged out"
- Check `auth.isAuthenticated` in Vue DevTools
- Verify both `auth.user` and `auth.token` are set
- If only token is set, check if `/api/auth/user` endpoint returns user data
### "Backend doesn't accept token"
- Verify backend expects: `Authorization: Bearer {token}`
- Check if backend route has auth middleware
- Test token manually: `curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8000/api/auth/user`
---
## No Plugin Required!
You mentioned Pinia Persisted or Vuex Persisted - you don't need them because:
1. Token is already saved in `localStorage` directly
2. Router guard reads from `localStorage` on app load
3. Token is automatically added to all API requests
4. This is simpler and more explicit than a plugin
The only state that needs to persist is the token (string), and we're handling that manually.

File diff suppressed because it is too large Load Diff

View File

@ -2,22 +2,12 @@
"name": "vue-soft-ui-dashboard-pro",
"version": "3.0.0",
"private": true,
"author": "Creative Tim",
"license": "SEE LICENSE IN <https://www.creative-tim.com/license>",
"description": "VueJS version of Soft UI Dashboard PRO by Creative Tim",
"homepage": "https://demos.creative-tim.com/vue-soft-ui-dashboard-pro/",
"bugs": {
"url": "https://github.com/creativetimofficial/ct-vue-soft-ui-dashboard-pro/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/creativetimofficial/ct-vue-soft-ui-dashboard-pro.git"
},
"author": "Creative Tim",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"typecheck": "vue-tsc --noEmit"
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "1.3.0",
@ -30,6 +20,7 @@
"@fullcalendar/interaction": "5.10.1",
"@fullcalendar/timegrid": "5.10.1",
"@popperjs/core": "2.10.2",
"axios": "^1.12.2",
"bootstrap": "5.1.3",
"chart.js": "3.6.0",
"choices.js": "9.0.1",
@ -39,6 +30,7 @@
"jkanban": "1.3.1",
"leaflet": "1.7.1",
"photoswipe": "4.1.3",
"pinia": "^2.0.36",
"quill": "1.3.6",
"round-slider": "1.6.1",
"simple-datatables": "3.2.0",
@ -54,15 +46,16 @@
"vuex": "4.0.2"
},
"devDependencies": {
"@types/minimatch": "^6.0.0",
"@types/node": "^24.6.0",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "4.5.15",
"@vue/cli-plugin-eslint": "4.5.15",
"@vue/cli-plugin-router": "4.5.15",
"@vue/cli-plugin-typescript": "^4.5.15",
"@vue/cli-plugin-typescript": "4.5.15",
"@vue/cli-service": "4.5.15",
"@vue/compiler-sfc": "3.2.0",
"@vue/eslint-config-prettier": "6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"babel-eslint": "10.1.0",
"eslint": "6.7.2",
"eslint-plugin-prettier": "3.3.1",
@ -70,7 +63,15 @@
"prettier": "2.2.1",
"sass": "1.43.3",
"sass-loader": "10.1.1",
"typescript": "^5.9.2",
"vue-tsc": "^3.1.0"
"typescript": "~4.1.5"
},
"bugs": {
"url": "https://github.com/creativetimofficial/ct-vue-soft-ui-dashboard-pro/issues"
},
"homepage": "https://demos.creative-tim.com/vue-soft-ui-dashboard-pro/",
"license": "SEE LICENSE IN <https://www.creative-tim.com/license>",
"repository": {
"type": "git",
"url": "git+https://github.com/creativetimofficial/ct-vue-soft-ui-dashboard-pro.git"
}
}

View File

@ -19,8 +19,7 @@ Coded by www.creative-tim.com
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet" />
<!-- Font Awesome Icons -->
<script src="https://kit.fontawesome.com/42d5adcbca.js" crossorigin="anonymous"></script>
<!-- Font Awesome Icons: using CSS only to avoid blocked kit.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css">

View File

@ -1,26 +1,34 @@
<template>
<sidenav
v-if="showSidenav"
:custom-class="color"
:class="[isTransparent, isRTL ? 'fixed-end' : 'fixed-start']"
/>
<main
class="main-content position-relative max-height-vh-100 h-100 border-radius-lg"
>
<!-- nav -->
<navbar
v-if="showNavbar"
:class="[isNavFixed ? navbarFixed : '', isAbsolute ? absolute : '']"
:text-white="isAbsolute ? 'text-white opacity-8' : ''"
:min-nav="navbarMinimize"
/>
<!-- Guest Layout: for login, signup, auth pages -->
<div v-if="isGuestRoute" class="guest-layout">
<router-view />
<app-footer v-show="showFooter" />
<configurator
:toggle="toggleConfigurator"
:class="[showConfig ? 'show' : '', hideConfigButton ? 'd-none' : '']"
</div>
<!-- Dashboard Layout: for authenticated pages -->
<div v-else class="dashboard-layout">
<sidenav
v-if="showSidenav"
:custom-class="color"
:class="[isTransparent, isRTL ? 'fixed-end' : 'fixed-start']"
/>
</main>
<main
class="main-content position-relative max-height-vh-100 h-100 border-radius-lg"
>
<!-- nav -->
<navbar
v-if="showNavbar"
:class="[isNavFixed ? navbarFixed : '', isAbsolute ? absolute : '']"
:text-white="isAbsolute ? 'text-white opacity-8' : ''"
:min-nav="navbarMinimize"
/>
<router-view />
<app-footer v-show="showFooter" />
<configurator
:toggle="toggleConfigurator"
:class="[showConfig ? 'show' : '', hideConfigButton ? 'd-none' : '']"
/>
</main>
</div>
</template>
<script>
import Sidenav from "./examples/Sidenav";
@ -51,6 +59,10 @@ export default {
"showConfig",
"hideConfigButton",
]),
isGuestRoute() {
// Check if current route has guestLayout meta flag
return this.$route.meta?.guestLayout === true;
},
},
beforeMount() {
this.$store.state.isTransparent = "bg-transparent";

View File

@ -0,0 +1,2 @@
// Custom custom styles for Soft UI Dashboard
// Leave empty or write additional styles.

View File

@ -0,0 +1,3 @@
// Custom variable overrides for Soft UI Dashboard
// Leave empty or override variables, e.g.:
// $primary: #5e72e4;

View File

@ -0,0 +1,108 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
>vue-cli documentation</a
>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
target="_blank"
rel="noopener"
>typescript</a
>
</li>
</ul>
<h3>Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
>Forum</a
>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
>Community Chat</a
>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
>Twitter</a
>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
>vue-router</a
>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank"
rel="noopener"
>vue-devtools</a
>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vue-loader</a
>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
rel="noopener"
>awesome-vue</a
>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "HelloWorld",
props: {
msg: String,
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<LoginTemplate>
<template #social-media>
<SocialMediaButtons />
</template>
<template #card-login>
<LoginForm />
</template>
</LoginTemplate>
</template>
<script setup lang="ts">
import LoginTemplate from "@/components/templates/LoginTemplate.vue";
import LoginForm from "@/components/molecules/auth/LoginForm.vue";
import SocialMediaButtons from "@/components/molecules/auth/SocialMedia.buttons.vue";
</script>

View File

@ -0,0 +1,115 @@
<template>
<div class="card-body">
<form role="form" class="text-start" @submit.prevent="handleLogin">
<div class="mb-3">
<SoftInput
id="email"
:value="email"
type="email"
placeholder="Email"
name="email"
:is-required="true"
@input="email = $event.target.value"
/>
</div>
<div class="mb-3">
<SoftInput
id="password"
:value="password"
name="password"
type="password"
placeholder="Mot de passe"
:is-required="true"
@input="password = $event.target.value"
/>
</div>
<SoftSwitch
id="rememberMe"
:checked="remember"
name="rememberMe"
@change="remember = $event.target.checked"
>
Souvenez de moi
</SoftSwitch>
<div v-if="errorMessage" class="alert alert-danger text-white" role="alert">
{{ errorMessage }}
</div>
<div class="text-center">
<SoftButton
type="submit"
class="my-4 mb-2"
variant="gradient"
color="info"
full-width
:disabled="isLoading"
>
{{ isLoading ? "Connexion..." : "Se connecter" }}
</SoftButton>
</div>
<div class="mb-2 text-center position-relative">
<p
class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2"
>
ou
</p>
</div>
<div class="text-center">
<SoftButton
class="mt-2 mb-4"
variant="gradient"
color="dark"
full-width
@click="$router.push('/authentication/signup/basic')"
>
creer un compte
</SoftButton>
</div>
</form>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import SoftInput from "@/components/SoftInput.vue";
import SoftSwitch from "@/components/SoftSwitch.vue";
import SoftButton from "@/components/SoftButton.vue";
const router = useRouter();
const authStore = useAuthStore();
const email = ref("");
const password = ref("");
const remember = ref(false);
const isLoading = ref(false);
const errorMessage = ref("");
const handleLogin = async () => {
if (!email.value || !password.value) {
errorMessage.value = "Veuillez remplir tous les champs";
return;
}
isLoading.value = true;
errorMessage.value = "";
try {
await authStore.login({
email: email.value,
password: password.value,
remember: remember.value,
});
// Redirect to dashboard on success
router.push("/dashboards/dashboard-default");
} catch (error: any) {
console.error("Login error:", error);
errorMessage.value =
error.response?.data?.message ||
error.message ||
"Email ou mot de passe incorrect";
} finally {
isLoading.value = false;
}
};
</script>

View File

@ -0,0 +1,67 @@
<template>
<div class="px-3 row px-xl-5 px-sm-4">
<div class="px-1 col-3 ms-auto">
<a class="btn btn-outline-light w-100" href="javascript:;">
<svg width="24px" height="32px" viewBox="0 0 64 64" version="1.1">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(3.000000, 3.000000)" fill-rule="nonzero">
<circle
fill="#3C5A9A"
cx="29.5091719"
cy="29.4927506"
r="29.4882047"
></circle>
<path
d="M39.0974944,9.05587273 L32.5651312,9.05587273 C28.6886088,9.05587273 24.3768224,10.6862851 24.3768224,16.3054653 C24.395747,18.2634019 24.3768224,20.1385313 24.3768224,22.2488655 L19.8922122,22.2488655 L19.8922122,29.3852113 L24.5156022,29.3852113 L24.5156022,49.9295284 L33.0113092,49.9295284 L33.0113092,29.2496356 L38.6187742,29.2496356 L39.1261316,22.2288395 L32.8649196,22.2288395 C32.8649196,22.2288395 32.8789377,19.1056932 32.8649196,18.1987181 C32.8649196,15.9781412 35.1755132,16.1053059 35.3144932,16.1053059 C36.4140178,16.1053059 38.5518876,16.1085101 39.1006986,16.1053059 L39.1006986,9.05587273 L39.0974944,9.05587273 L39.0974944,9.05587273 Z"
fill="#FFFFFF"
></path>
</g>
</g>
</svg>
</a>
</div>
<div class="px-1 col-3">
<a class="btn btn-outline-light w-100" href="javascript:;">
<svg width="24px" height="32px" viewBox="0 0 64 64" version="1.1">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g
transform="translate(7.000000, 0.564551)"
fill="#000000"
fill-rule="nonzero"
>
<path
d="M40.9233048,32.8428307 C41.0078713,42.0741676 48.9124247,45.146088 49,45.1851909 C48.9331634,45.4017274 47.7369821,49.5628653 44.835501,53.8610269 C42.3271952,57.5771105 39.7241148,61.2793611 35.6233362,61.356042 C31.5939073,61.431307 30.2982233,58.9340578 25.6914424,58.9340578 C21.0860585,58.9340578 19.6464932,61.27947 15.8321878,61.4314159 C11.8738936,61.5833617 8.85958554,57.4131833 6.33064852,53.7107148 C1.16284874,46.1373849 -2.78641926,32.3103122 2.51645059,22.9768066 C5.15080028,18.3417501 9.85858819,15.4066355 14.9684701,15.3313705 C18.8554146,15.2562145 22.5241194,17.9820905 24.9003639,17.9820905 C27.275104,17.9820905 31.733383,14.7039812 36.4203248,15.1854154 C38.3824403,15.2681959 43.8902255,15.9888223 47.4267616,21.2362369 C47.1417927,21.4153043 40.8549638,25.1251794 40.9233048,32.8428307 M33.3504628,10.1750144 C35.4519466,7.59650964 36.8663676,4.00699306 36.4804992,0.435448578 C33.4513624,0.558856931 29.7884601,2.48154382 27.6157341,5.05863265 C25.6685547,7.34076135 23.9632549,10.9934525 24.4233742,14.4943068 C27.7996959,14.7590956 31.2488715,12.7551531 33.3504628,10.1750144"
></path>
</g>
</g>
</svg>
</a>
</div>
<div class="px-1 col-3 me-auto">
<a class="btn btn-outline-light w-100" href="javascript:;">
<svg width="24px" height="32px" viewBox="0 0 64 64" version="1.1">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(3.000000, 2.000000)" fill-rule="nonzero">
<path
d="M57.8123233,30.1515267 C57.8123233,27.7263183 57.6155321,25.9565533 57.1896408,24.1212666 L29.4960833,24.1212666 L29.4960833,35.0674653 L45.7515771,35.0674653 C45.4239683,37.7877475 43.6542033,41.8844383 39.7213169,44.6372555 L39.6661883,45.0037254 L48.4223791,51.7870338 L49.0290201,51.8475849 C54.6004021,46.7020943 57.8123233,39.1313952 57.8123233,30.1515267"
fill="#4285F4"
></path>
<path
d="M29.4960833,58.9921667 C37.4599129,58.9921667 44.1456164,56.3701671 49.0290201,51.8475849 L39.7213169,44.6372555 C37.2305867,46.3742596 33.887622,47.5868638 29.4960833,47.5868638 C21.6960582,47.5868638 15.0758763,42.4415991 12.7159637,35.3297782 L12.3700541,35.3591501 L3.26524241,42.4054492 L3.14617358,42.736447 C7.9965904,52.3717589 17.959737,58.9921667 29.4960833,58.9921667"
fill="#34A853"
></path>
<path
d="M12.7159637,35.3297782 C12.0932812,33.4944915 11.7329116,31.5279353 11.7329116,29.4960833 C11.7329116,27.4640054 12.0932812,25.4976752 12.6832029,23.6623884 L12.6667095,23.2715173 L3.44779955,16.1120237 L3.14617358,16.2554937 C1.14708246,20.2539019 0,24.7439491 0,29.4960833 C0,34.2482175 1.14708246,38.7380388 3.14617358,42.736447 L12.7159637,35.3297782"
fill="#FBBC05"
></path>
<path
d="M29.4960833,11.4050769 C35.0347044,11.4050769 38.7707997,13.7975244 40.9011602,15.7968415 L49.2255853,7.66898166 C44.1130815,2.91684746 37.4599129,0 29.4960833,0 C17.959737,0 7.9965904,6.62018183 3.14617358,16.2554937 L12.6832029,23.6623884 C15.0758763,16.5505675 21.6960582,11.4050769 29.4960833,11.4050769"
fill="#EB4335"
></path>
</g>
</g>
</svg>
</a>
</div>
</div>
</template>

View File

@ -0,0 +1,34 @@
<template>
<div
class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg"
:style="{
backgroundImage:
'url(' + require('@/assets/img/curved-images/curved9.jpg') + ')',
}"
>
<span class="mask bg-gradient-dark opacity-6"></span>
<div class="container">
<div class="row justify-content-center">
<div class="mx-auto text-center col-lg-5">
<h1 class="mt-5 mb-2 text-white">Bonjour !</h1>
<p class="text-white text-lead">
Veuillez entrer vos identifiants pour vous connecter
</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row mt-lg-n10 mt-md-n11 mt-n10 justify-content-center">
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
<div class="card z-index-0">
<div class="pt-4 text-center card-header">
<h5>Connectez-vous</h5>
</div>
<slot name="social-media" />
<slot name="card-login" />
</div>
</div>
</div>
</div>
</template>

View File

@ -55,16 +55,16 @@
</div>
<ul class="navbar-nav justify-content-end">
<li class="nav-item d-flex align-items-center">
<router-link
:to="{ name: 'Signin Basic' }"
<a
href="#"
class="px-0 nav-link font-weight-bold"
:class="textWhite ? textWhite : 'text-body'"
target="_blank"
@click.prevent="handleLogout"
>
<i class="fa fa-user" :class="isRTL ? 'ms-sm-2' : 'me-sm-1'"></i>
<span v-if="isRTL" class="d-sm-inline d-none">يسجل دخول</span>
<span v-else class="d-sm-inline d-none">Sign In </span>
</router-link>
<i class="fa fa-sign-out-alt" :class="isRTL ? 'ms-sm-2' : 'me-sm-1'"></i>
<span v-if="isRTL" class="d-sm-inline d-none">تسجيل خروج</span>
<span v-else class="d-sm-inline d-none">Logout</span>
</a>
</li>
<li class="nav-item d-xl-none ps-3 d-flex align-items-center">
<a
@ -223,6 +223,7 @@
<script>
import Breadcrumbs from "../Breadcrumbs.vue";
import { mapMutations, mapActions, mapState } from "vuex";
import { useAuthStore } from "@/stores/auth";
export default {
name: "Navbar",
@ -265,6 +266,19 @@ export default {
this.toggleSidebarColor("bg-white");
this.navbarMinimize();
},
async handleLogout() {
try {
const authStore = useAuthStore();
await authStore.logout();
// Redirect to login page
this.$router.push("/login");
} catch (error) {
console.error("Logout error:", error);
// Still redirect to login even if logout API fails
this.$router.push("/login");
}
},
},
};
</script>

View File

@ -9,7 +9,8 @@
class="navbar-brand font-weight-bolder ms-lg-0 ms-3"
:class="darkMode ? 'text-black' : 'text-white'"
to="/"
>Soft UI Dashboard PRO</router-link>
>Soft UI Dashboard PRO</router-link
>
<button
class="shadow-none navbar-toggler ms-2"
type="button"

View File

@ -15,11 +15,9 @@
>
<slot name="icon"></slot>
</div>
<span
class="nav-link-text"
:class="isRTL ? ' me-1' : 'ms-1'"
>{{ navText }}</span
>
<span class="nav-link-text" :class="isRTL ? ' me-1' : 'ms-1'">{{
navText
}}</span>
</a>
<div :id="collapseRef" class="collapse">
<slot name="list"></slot>
@ -32,16 +30,16 @@ export default {
props: {
collapseRef: {
type: String,
required: true
required: true,
},
navText: {
type: String,
required: true
required: true,
},
collapse: {
type: Boolean,
default: true
}
default: true,
},
},
computed: {
...mapState(["isRTL"]),

View File

@ -24,16 +24,16 @@ export default {
props: {
refer: {
type: String,
required: true
required: true,
},
miniIcon: {
type: String,
required: true
required: true,
},
text: {
type: String,
required: true
}
}
required: true,
},
},
};
</script>

View File

@ -844,9 +844,9 @@ export default {
default: "",
},
},
computed: {
...mapState(["isRTL"]),
},
computed: {
...mapState(["isRTL"]),
},
methods: {
getRoute() {
const routeArr = this.$route.path.split("/");

View File

@ -18,8 +18,10 @@ import "./assets/css/nucleo-svg.css";
import VueTilt from "vue-tilt.js";
import VueSweetalert2 from "vue-sweetalert2";
import SoftUIDashboard from "./soft-ui-dashboard";
import pinia from "./plugins/pinia";
const appInstance = createApp(App);
appInstance.use(pinia);
appInstance.use(store);
appInstance.use(router);
appInstance.use(VueTilt);

View File

@ -0,0 +1,5 @@
import { createPinia } from 'pinia'
// Single shared Pinia instance so router guards and app use the same store
export const pinia = createPinia()
export default pinia

View File

@ -54,12 +54,30 @@ import lockBasic from "../views/auth/lock/Basic.vue";
import lockCover from "../views/auth/lock/Cover.vue";
import lockIllustration from "../views/auth/lock/Illustration.vue";
//ROUTE SHOULD USED
const routes = [
{
path: "/",
name: "/",
redirect: "/dashboards/dashboard-default",
},
{
path: "/dashboard",
redirect: "/dashboards/dashboard-default",
},
{
path: "/login",
name: "Login",
component: () => import("@/views/pages/Login.vue"),
meta: { public: true, guestOnly: true, guestLayout: true },
},
{
path: "/register",
name: "Register",
component: () => import("@/views/pages/Register.vue"),
meta: { public: true, guestOnly: true, guestLayout: true },
},
{
path: "/dashboards/dashboard-default",
name: "Default",
@ -249,94 +267,158 @@ const routes = [
path: "/authentication/signin/basic",
name: "Signin Basic",
component: Basic,
meta: { guestLayout: true },
},
{
path: "/authentication/signin/cover",
name: "Signin Cover",
component: Cover,
meta: { guestLayout: true },
},
{
path: "/authentication/signin/illustration",
name: "Signin Illustration",
component: Illustration,
meta: { guestLayout: true },
},
{
path: "/authentication/reset/basic",
name: "Reset Basic",
component: ResetBasic,
meta: { guestLayout: true },
},
{
path: "/authentication/reset/cover",
name: "Reset Cover",
component: ResetCover,
meta: { guestLayout: true },
},
{
path: "/authentication/reset/illustration",
name: "Reset Illustration",
component: ResetIllustration,
meta: { guestLayout: true },
},
{
path: "/authentication/lock/basic",
name: "Lock Basic",
component: lockBasic,
meta: { guestLayout: true },
},
{
path: "/authentication/lock/cover",
name: "Lock Cover",
component: lockCover,
meta: { guestLayout: true },
},
{
path: "/authentication/lock/illustration",
name: "Lock Illustration",
component: lockIllustration,
meta: { guestLayout: true },
},
{
path: "/authentication/verification/basic",
name: "Verification Basic",
component: VerificationBasic,
meta: { guestLayout: true },
},
{
path: "/authentication/verification/cover",
name: "Verification Cover",
component: VerificationCover,
meta: { guestLayout: true },
},
{
path: "/authentication/verification/illustration",
name: "Verification Illustration",
component: VerificationIllustration,
meta: { guestLayout: true },
},
{
path: "/authentication/signup/basic",
name: "Signup Basic",
component: SignupBasic,
meta: { guestLayout: true },
},
{
path: "/authentication/signup/cover",
name: "Signup Cover",
component: SignupCover,
meta: { guestLayout: true },
},
{
path: "/authentication/signup/illustration",
name: "Signup Illustration",
component: SignupIllustration,
meta: { guestLayout: true },
},
{
path: "/authentication/error/error404",
name: "Error Error404",
component: Error404,
meta: { guestLayout: true },
},
{
path: "/authentication/error/error500",
name: "Error Error500",
component: Error500,
meta: { guestLayout: true },
},
];
const router = createRouter({
// Use root base so the app works whether served at / or via /build/index.html
history: createWebHistory("/"),
history: createWebHistory(process.env.BASE_URL),
routes,
linkActiveClass: "active",
});
// Auth guard using Pinia store and Laravel Sanctum session
import { useAuthStore } from "@/stores/auth";
import pinia from "@/plugins/pinia";
const DASHBOARD = "/dashboards/dashboard-default";
const LOGIN = "/login"; // canonical login path without redirect query
router.beforeEach(async (to, from, next) => {
const auth = useAuthStore(pinia);
const isPublic = to.matched.some((r) => r.meta && r.meta.public === true);
const isGuestOnly = to.matched.some(
(r) => r.meta && r.meta.guestOnly === true
);
const isLoginRoute = to.name === "Login" || to.name === "Signin";
// Initialize auth from localStorage token if not already done
if (!auth.checked) {
const token = localStorage.getItem("auth_token");
if (token) {
// Token exists, set it and try to fetch user
auth.token = token;
try {
await auth.fetchMe();
} catch (error) {
// If token is invalid, clear it and continue as unauthenticated
console.warn("Invalid token, clearing auth");
localStorage.removeItem("auth_token");
auth.token = null;
auth.user = null;
auth.checked = true;
}
} else {
// No token, mark as checked
auth.checked = true;
}
}
if (auth.isAuthenticated) {
// Prevent authenticated users from visiting guest-only routes (like sign-in)
if (isGuestOnly || isLoginRoute) return next(DASHBOARD);
return next();
}
// Not authenticated: allow public/guest routes (like sign-in), otherwise send to login (no redirect param)
if (isPublic || isGuestOnly || isLoginRoute) return next();
return next({ path: LOGIN, replace: true });
});
export default router;

View File

@ -0,0 +1,92 @@
import { request, http } from "./http";
export interface LoginPayload {
email: string;
password: string;
remember?: boolean;
}
export interface RegisterPayload {
name: string;
email: string;
password: string;
}
export interface User {
id: number;
name: string;
email: string;
email_verified_at: string | null;
created_at: string;
updated_at: string;
}
export interface LoginResponse {
success: boolean;
data: {
user: User;
token: string;
};
message: string;
}
export const AuthService = {
async login(payload: LoginPayload): Promise<LoginResponse> {
const response = await request<LoginResponse>({
url: "/api/auth/login",
method: "post",
data: payload,
});
// Save token to localStorage
if (response.success && response.data.token) {
localStorage.setItem("auth_token", response.data.token);
}
return response;
},
async register(payload: RegisterPayload): Promise<LoginResponse> {
const response = await request<LoginResponse>({
url: "/api/auth/register",
method: "post",
data: payload,
});
// Save token to localStorage (user is automatically logged in after registration)
if (response.success && response.data.token) {
localStorage.setItem("auth_token", response.data.token);
}
return response;
},
async logout() {
try {
await request<void>({ url: "/api/auth/logout", method: "post" });
} finally {
// Always remove token from localStorage on logout
localStorage.removeItem("auth_token");
}
},
async me() {
// Fetch current user from API
const response = await request<any>({ url: "/api/auth/user", method: "get" });
// Handle both direct user response and wrapped response
if (response.success && response.data) {
return response.data;
}
return response;
},
getToken(): string | null {
return localStorage.getItem("auth_token");
},
hasToken(): boolean {
return !!this.getToken();
},
};
export default AuthService;

View File

@ -0,0 +1,66 @@
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
// Read base URL from Vue CLI env (must start with VUE_APP_)
const baseURL = process.env.VUE_APP_API_BASE_URL || "";
// Laravel Sanctum defaults
// - xsrfCookieName: 'XSRF-TOKEN'
// - xsrfHeaderName: 'X-XSRF-TOKEN'
// - withCredentials: true (needed for cookie-based session)
let csrfInitPromise: Promise<void> | null = null;
async function ensureCSRF(client: AxiosInstance) {
if (!csrfInitPromise) {
// Hitting /sanctum/csrf-cookie sets the XSRF-TOKEN cookie used by Axios
csrfInitPromise = client
.get("/sanctum/csrf-cookie", { withCredentials: true })
.then(() => void 0);
}
return csrfInitPromise;
}
export const http: AxiosInstance = axios.create({
baseURL,
withCredentials: true,
// These match Laravels defaults and Axios defaults; set explicitly for clarity
xsrfCookieName: "XSRF-TOKEN",
xsrfHeaderName: "X-XSRF-TOKEN",
headers: {
"X-Requested-With": "XMLHttpRequest",
Accept: "application/json",
},
timeout: 30000,
});
// Optionally attach a request interceptor to ensure CSRF for state-changing requests
http.interceptors.request.use(async (config) => {
// Add Bearer token if available
const token = localStorage.getItem("auth_token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Only ensure CSRF for unsafe methods (POST, PUT, PATCH, DELETE)
// Skip CSRF if using token-based auth
const method = (config.method || "get").toLowerCase();
if (!token && method !== "get" && method !== "head" && method !== "options") {
await ensureCSRF(http);
}
return config;
});
// Basic response error normalization (optional)
http.interceptors.response.use(
(r) => r,
(error: AxiosError) => {
// You can add global error handling, e.g., redirect on 401, etc.
return Promise.reject(error);
}
);
// Helper to build typed requests if needed
export async function request<T = any>(config: AxiosRequestConfig) {
const response = await http.request<T>(config);
return response.data;
}

View File

@ -0,0 +1,2 @@
export * from './http'
export { default as AuthService } from './auth'

View File

@ -1,35 +1,13 @@
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module "*.png" {
const src: string;
export default src;
}
declare module "*.jpg" {
const src: string;
export default src;
}
declare module "*.jpeg" {
const src: string;
export default src;
}
declare module "*.gif" {
const src: string;
export default src;
}
declare module "*.webp" {
const src: string;
export default src;
}
declare module "*.svg" {
const src: string;
export default src;
// Type shim for packages without TypeScript declarations
declare module 'vue-tilt.js' {
import type { Plugin } from 'vue'
const VueTilt: Plugin
export default VueTilt
}

View File

@ -19,7 +19,7 @@ export default createStore({
navbarFixed:
"position-sticky blur shadow-blur left-auto top-1 z-index-sticky px-0 mx-4",
absolute: "position-absolute px-4 mx-0 w-100 z-index-2",
bootstrap
bootstrap,
},
mutations: {
toggleConfigurator(state) {
@ -57,7 +57,7 @@ export default createStore({
},
toggleHideConfig(state) {
state.hideConfigButton = !state.hideConfigButton;
}
},
},
actions: {
toggleSidebarColor({ commit }, payload) {
@ -67,5 +67,5 @@ export default createStore({
commit("cardBackground", payload);
},
},
getters: {}
getters: {},
});

View File

@ -0,0 +1,86 @@
import { defineStore } from "pinia";
import type { User, LoginPayload, RegisterPayload } from "@/services/auth";
import AuthService from "@/services/auth";
export const useAuthStore = defineStore("auth", {
state: () => ({
user: null as User | null,
token: null as string | null,
checked: false as boolean, // whether we've attempted to fetch current user
}),
getters: {
isAuthenticated: (state) => !!state.user && !!state.token,
},
actions: {
async fetchMe() {
try {
const userData = await AuthService.me();
// Validate that we actually received a user object, not HTML or other invalid data
if (userData && typeof userData === 'object' && 'id' in userData && 'email' in userData) {
this.user = userData;
} else {
// If backend returned invalid data (like HTML), treat as unauthenticated
console.warn('Invalid user data received from /api/user:', userData);
this.user = null;
}
} catch (e) {
console.error('Error fetching user:', e);
this.user = null;
this.token = null;
} finally {
this.checked = true;
}
return this.user;
},
async login(payload: LoginPayload) {
const response = await AuthService.login(payload);
if (response.success && response.data.user && response.data.token) {
this.user = response.data.user;
this.token = response.data.token;
this.checked = true;
return this.user;
} else {
throw new Error(response.message || "Login failed");
}
},
async register(payload: RegisterPayload) {
const response = await AuthService.register(payload);
if (response.success && response.data.user && response.data.token) {
this.user = response.data.user;
this.token = response.data.token;
this.checked = true;
return this.user;
} else {
throw new Error(response.message || "Registration failed");
}
},
async logout() {
try {
await AuthService.logout();
} finally {
this.user = null;
this.token = null;
this.checked = true;
}
},
// Initialize auth state from localStorage token on app load
initializeAuth() {
const token = AuthService.getToken();
if (token) {
this.token = token;
// Optionally fetch user data if token exists
this.fetchMe().catch(() => {
// If fetching user fails, clear invalid token
this.token = null;
AuthService.logout();
});
} else {
this.checked = true;
}
},
},
});
export default useAuthStore;

View File

@ -1,8 +0,0 @@
export interface User {
id: number;
name: string;
email: string;
email_verified_at?: string | null;
created_at?: string;
updated_at?: string;
}

View File

@ -1,7 +1,18 @@
<template>
<default-dashboard />
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
</template>
<script setup lang="ts">
import DefaultDashboard from "@/views/dashboards/Default.vue";
<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
export default defineComponent({
name: "Home",
components: {
HelloWorld,
},
});
</script>

View File

@ -40,11 +40,11 @@
value="930"
:percentage="{
value: '+55%',
color: 'text-success'
color: 'text-success',
}"
:icon="{
component: 'ni ni-circle-08',
background: 'bg-gradient-dark'
background: 'bg-gradient-dark',
}"
class="ms-1"
direction-reverse
@ -56,11 +56,11 @@
value="744"
:percentage="{
value: '+3%',
color: 'text-success'
color: 'text-success',
}"
:icon="{
component: 'ni ni-world',
background: 'bg-gradient-dark'
background: 'bg-gradient-dark',
}"
class="ms-1"
direction-reverse
@ -72,11 +72,11 @@
value="1,414"
:percentage="{
value: '-2%',
color: 'text-danger'
color: 'text-danger',
}"
:icon="{
component: 'ni ni-watch-time',
background: 'bg-gradient-dark'
background: 'bg-gradient-dark',
}"
class="ms-1"
direction-reverse
@ -88,11 +88,11 @@
value="1.76"
:percentage="{
value: '+5%',
color: 'text-success'
color: 'text-success',
}"
:icon="{
component: 'ni ni-image',
background: 'bg-gradient-dark'
background: 'bg-gradient-dark',
}"
class="ms-1"
direction-reverse
@ -133,22 +133,22 @@
'Sep',
'Oct',
'Nov',
'Dec'
'Dec',
],
datasets: [
{
label: 'Organic Search',
data: [50, 40, 300, 220, 500, 250, 400, 230, 500]
data: [50, 40, 300, 220, 500, 250, 400, 230, 500],
},
{
label: 'Referral',
data: [30, 90, 40, 140, 290, 290, 340, 230, 400]
data: [30, 90, 40, 140, 290, 290, 340, 230, 400],
},
{
label: 'Direct',
data: [40, 80, 70, 90, 30, 90, 140, 130, 200]
}
]
data: [40, 80, 70, 90, 30, 90, 140, 130, 200],
},
],
}"
/>
</div>
@ -160,12 +160,12 @@
title="Refferals"
:chart="{
labels: ['Adobe', 'Atlassian', 'Slack', 'Spotify', 'Jira'],
datasets: [{ label: 'Referrals', data: [25, 3, 12, 7, 10] }]
datasets: [{ label: 'Referrals', data: [25, 3, 12, 7, 10] }],
}"
:actions="{
route: 'https://creative-tim.com',
label: 'See all referrals',
color: 'secondary'
color: 'secondary',
}"
/>
</div>
@ -177,28 +177,28 @@
{
label: 'Facebook',
icon: 'facebook',
progress: 80
progress: 80,
},
{
label: 'Twitter',
icon: 'twitter',
progress: 40
progress: 40,
},
{
label: 'Reddit',
icon: 'reddit',
progress: 30
progress: 30,
},
{
label: 'Youtube',
icon: 'youtube',
progress: 25
progress: 25,
},
{
label: 'Slack',
icon: 'slack',
progress: 15
}
progress: 15,
},
]"
/>
</div>
@ -209,44 +209,44 @@
url: '/bits',
views: 345,
time: '00:17:07',
rate: '40.91%'
rate: '40.91%',
},
{
url: '/pages/argon-dashboard',
views: 520,
time: '00:23:13',
rate: '30.14%'
rate: '30.14%',
},
{
url: '/pages/soft-ui-dashboard',
views: 122,
time: '00:3:10',
rate: '54.10%'
rate: '54.10%',
},
{
url: '/bootstrap-themes',
views: '1,900',
time: '00:30:42',
rate: '20.93%'
rate: '20.93%',
},
{
url: '/react-themes',
views: '1,442',
time: '00:31:50',
rate: '34.98%'
rate: '34.98%',
},
{
url: '/product/argon-dashboard-angular',
views: 201,
time: '00:12:42',
rate: '21.4%'
rate: '21.4%',
},
{
url: '/product/material-dashboard-pro',
views: '2,115',
time: '00:50:11',
rate: '34.98%'
}
rate: '34.98%',
},
]"
/>
</div>
@ -274,7 +274,7 @@ export default {
DefaultLineChart,
DefaultDoughnutChart,
SocialCard,
PagesCard
PagesCard,
},
data() {
return {
@ -282,11 +282,11 @@ export default {
logoAtlassian,
logoSlack,
logoSpotify,
logoJira
logoJira,
};
},
mounted() {
setTooltip(this.$store.state.bootstrap);
}
},
};
</script>

View File

@ -1,5 +1,4 @@
<template>
<navbar btn-background="bg-gradient-success" />
<div
class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg"
:style="{
@ -11,10 +10,9 @@
<div class="container">
<div class="row justify-content-center">
<div class="mx-auto text-center col-lg-5">
<h1 class="mt-5 mb-2 text-white">Welcome!</h1>
<h1 class="mt-5 mb-2 text-white">Bonjour !</h1>
<p class="text-white text-lead">
Use these awesome forms to login or create new account in your
project for free.
Veuillez entrer vos identifiants pour vous connecter
</p>
</div>
</div>
@ -25,7 +23,7 @@
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
<div class="card z-index-0">
<div class="pt-4 text-center card-header">
<h5>Sign in</h5>
<h5>Connectez-vous</h5>
</div>
<div class="px-3 row px-xl-5 px-sm-4">
<div class="px-1 col-3 ms-auto">
@ -143,11 +141,11 @@
id="password"
name="password"
type="password"
placeholder="Password"
placeholder="Mot de passe"
/>
</div>
<soft-switch id="rememberMe" name="rememberMe">
Remember me
Souvenez de moi
</soft-switch>
<div class="text-center">
<soft-button
@ -155,14 +153,14 @@
variant="gradient"
color="info"
full-width
>Sign in
>Se connecter
</soft-button>
</div>
<div class="mb-2 text-center position-relative">
<p
class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2"
>
or
ou
</p>
</div>
<div class="text-center">
@ -171,7 +169,7 @@
variant="gradient"
color="dark"
full-width
>Sign up
>creer un compte
</soft-button>
</div>
</form>
@ -180,37 +178,29 @@
</div>
</div>
</div>
<app-footer />
</template>
<script>
import Navbar from "@/examples/PageLayout/Navbar.vue";
import AppFooter from "@/examples/PageLayout/Footer.vue";
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from "vue";
import { useStore } from "vuex";
import SoftInput from "@/components/SoftInput.vue";
import SoftSwitch from "@/components/SoftSwitch.vue";
import SoftButton from "@/components/SoftButton.vue";
import { mapMutations } from "vuex";
export default {
name: "SigninBasic",
components: {
Navbar,
AppFooter,
SoftInput,
SoftSwitch,
SoftButton,
},
const store = useStore();
created() {
this.toggleEveryDisplay();
this.toggleHideConfig();
},
beforeUnmount() {
this.toggleEveryDisplay();
this.toggleHideConfig();
},
methods: {
...mapMutations(["toggleEveryDisplay", "toggleHideConfig"]),
},
};
// Map mutations
const toggleEveryDisplay = () => store.commit("toggleEveryDisplay");
const toggleHideConfig = () => store.commit("toggleHideConfig");
// Lifecycle hooks
onMounted(() => {
toggleEveryDisplay();
toggleHideConfig();
});
onBeforeUnmount(() => {
toggleEveryDisplay();
toggleHideConfig();
});
</script>

View File

@ -24,13 +24,19 @@
<p class="mb-0">Enter your email and password to sign in</p>
</div>
<div class="card-body">
<form role="form" class="text-start">
<form
role="form"
class="text-start"
@submit.prevent="onSubmit"
>
<label>Email</label>
<soft-input
id="email"
type="email"
placeholder="Email"
name="email"
:value="email"
@input="onEmailInput"
/>
<label>Password</label>
<soft-input
@ -38,8 +44,15 @@
type="password"
placeholder="Password"
name="password"
:value="password"
@input="onPasswordInput"
/>
<soft-switch id="rememberMe" name="rememberMe" checked>
<soft-switch
id="rememberMe"
name="rememberMe"
:checked="remember"
@change="onRememberChange"
>
Remember me
</soft-switch>
<div class="text-center">
@ -54,14 +67,7 @@
</form>
</div>
<div class="px-1 pt-0 text-center card-footer px-lg-2">
<p class="mx-auto mb-4 text-sm">
Don't have an account?
<router-link
:to="{ name: 'Signup Cover' }"
class="text-success text-gradient font-weight-bold"
>Sign up</router-link
>
</p>
<p class="mx-auto mb-4 text-sm">Don't have an account?</p>
</div>
</div>
</div>
@ -94,6 +100,7 @@ import AppFooter from "@/examples/PageLayout/Footer.vue";
import SoftInput from "@/components/SoftInput.vue";
import SoftSwitch from "@/components/SoftSwitch.vue";
import SoftButton from "@/components/SoftButton.vue";
import { useAuthStore } from "@/stores/auth";
const body = document.getElementsByTagName("body")[0];
import { mapMutations } from "vuex";
@ -116,8 +123,39 @@ export default {
this.toggleHideConfig();
body.classList.add("bg-gray-100");
},
data() {
return {
email: "",
password: "",
remember: true,
};
},
methods: {
...mapMutations(["toggleEveryDisplay", "toggleHideConfig"]),
onEmailInput(e) {
this.email = e.target.value;
},
onPasswordInput(e) {
this.password = e.target.value;
},
onRememberChange(e) {
this.remember = e.target.checked;
},
async onSubmit() {
try {
const auth = useAuthStore();
await auth.login({
email: this.email,
password: this.password,
remember: this.remember,
});
const redirect = this.$route.query.redirect || "/dashboard";
this.$router.push(redirect);
} catch (e) {
// Optionally show a message to the user
console.error("Login failed", e);
}
},
},
};
</script>

View File

@ -11,15 +11,15 @@
class="bg-gradient-secondary"
:title="{
text: 'Today\'s Trip',
color: 'opacity-7 text-white'
color: 'opacity-7 text-white',
}"
:value="{
text: '145 Km',
color: 'text-white'
color: 'text-white',
}"
:icon="{
component: 'text-dark ni ni-money-coins',
background: 'bg-white'
background: 'bg-white',
}"
direction-reverse
/>
@ -29,15 +29,15 @@
class="bg-gradient-secondary"
:title="{
text: 'Battery Health',
color: 'opacity-7 text-white'
color: 'opacity-7 text-white',
}"
:value="{
text: '99 %',
color: 'text-white'
color: 'text-white',
}"
:icon="{
component: 'text-dark ni ni-controller',
background: 'bg-white'
background: 'bg-white',
}"
direction-reverse
/>
@ -47,15 +47,15 @@
class="bg-gradient-secondary"
:title="{
text: 'Average Speed',
color: 'opacity-7 text-white'
color: 'opacity-7 text-white',
}"
:value="{
text: '56 Km/h',
color: 'text-white'
color: 'text-white',
}"
:icon="{
component: 'text-dark ni ni-delivery-fast',
background: 'bg-white'
background: 'bg-white',
}"
direction-reverse
/>
@ -65,15 +65,15 @@
class="bg-gradient-secondary"
:title="{
text: 'Music Volume',
color: 'opacity-7 text-white'
color: 'opacity-7 text-white',
}"
:value="{
text: '15/100',
color: 'text-white'
color: 'text-white',
}"
:icon="{
component: 'text-dark ni ni-note-03',
background: 'bg-white'
background: 'bg-white',
}"
direction-reverse
/>
@ -98,10 +98,10 @@ export default {
components: {
MiniStatisticsCard,
PlayerCard,
CardDetail
CardDetail,
},
mounted() {
setTooltip(this.$store.state.bootstrap);
}
},
};
</script>

View File

@ -203,7 +203,7 @@
</div>
</div>
</template>
<script setupe lang="ts">
<script>
import MiniStatisticsCard from "../../examples/Cards/MiniStatisticsCard.vue";
import ReportsBarChart from "../../examples/Charts/ReportsBarChart.vue";
import GradientLineChart from "../../examples/Charts/GradientLineChart.vue";

View File

@ -94,7 +94,7 @@
:style="{
backgroundImage:
'url(' + require('@/assets/img/bg-smart-home-1.jpg') + ')',
backgroundSize: 'cover'
backgroundSize: 'cover',
}"
>
<div class="top-0 position-absolute d-flex w-100">
@ -115,7 +115,7 @@
:style="{
backgroundImage:
'url(' + require('@/assets/img/bg-smart-home-2.jpg') + ')',
backgroundSize: 'cover'
backgroundSize: 'cover',
}"
>
<div class="top-0 position-absolute d-flex w-100">
@ -136,7 +136,7 @@
:style="{
backgroundImage:
'url(' + require('@/assets/img/home-decor-3.jpg') + ')',
backgroundSize: 'cover'
backgroundSize: 'cover',
}"
>
<div class="top-0 position-absolute d-flex w-100">
@ -235,8 +235,8 @@
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
datasets: {
label: 'Watts',
data: [150, 230, 380, 220, 420, 200, 70, 500]
}
data: [150, 230, 380, 220, 420, 200, 70, 500],
},
}"
/>
</div>
@ -270,7 +270,7 @@
state: 'Off',
label: 'Humidity',
description: 'Inactive since: 2 days',
classCustom: 'mt-4'
classCustom: 'mt-4',
}"
>
<humidity />
@ -283,7 +283,7 @@
label: 'Temperature',
description: 'Active',
classCustom: 'mt-2',
isChecked: 'true'
isChecked: 'true',
}"
class="text-white bg-gradient-success"
>
@ -297,7 +297,7 @@
label: 'Air Conditioner',
description: 'Inactive since: 1 hour',
classCustom: 'mt-4',
isChecked: true
isChecked: true,
}"
>
<air />
@ -309,7 +309,7 @@
state: 'Off',
label: 'Lights',
description: 'Inactive since: 27 min',
classCustom: 'mt-4'
classCustom: 'mt-4',
}"
>
<lights />
@ -322,7 +322,7 @@
label: 'Wi-fi',
description: 'Active',
classCustom: 'mt-4',
isChecked: true
isChecked: true,
}"
class="text-white bg-gradient-success"
>
@ -374,11 +374,11 @@ export default {
Air,
Lights,
Wifi,
Humidity
Humidity,
},
data() {
return {
showMenu: false
showMenu: false,
};
},
mounted() {
@ -405,7 +405,7 @@ export default {
sliderType: "min-range",
lineCap: "round",
min: 16,
max: 38
max: 38,
});
// Rounded slider
const setValue = function (value, active) {
@ -436,6 +436,6 @@ export default {
else if (ev.detail.high !== undefined) setHigh(ev.detail.high, true);
});
});
}
},
};
</script>

View File

@ -10,7 +10,7 @@
class="mx-3 mt-3 border-radius-xl position-relative"
:style="{
backgroundImage: 'url(' + require('../../../assets/img/vr-bg.jpg') + ')',
backgroundSize: 'cover'
backgroundSize: 'cover',
}"
>
<sidenav
@ -86,16 +86,16 @@
:items="[
{
time: '08:00',
description: `Synk up with Mark<small class='text-secondary font-weight-normal'>Hangouts</small>`
description: `Synk up with Mark<small class='text-secondary font-weight-normal'>Hangouts</small>`,
},
{
time: '09:30',
description: `Gym<br /><small class='text-secondary font-weight-normal'>World Class</small>`
description: `Gym<br /><small class='text-secondary font-weight-normal'>World Class</small>`,
},
{
time: '11:00',
description: `Design Review<br /><small class='text-secondary font-weight-normal'>Zoom</small>`
}
description: `Design Review<br /><small class='text-secondary font-weight-normal'>Zoom</small>`,
},
]"
/>
</div>
@ -110,23 +110,23 @@
{
route: '/',
tooltip: '2 New Messages',
image: { url: image1, alt: 'Image Placeholder' }
image: { url: image1, alt: 'Image Placeholder' },
},
{
route: '/',
tooltip: '1 New Messages',
image: { url: image2, alt: 'Image Placeholder' }
image: { url: image2, alt: 'Image Placeholder' },
},
{
route: '/',
tooltip: '13 New Messages',
image: { url: image3, alt: 'Image Placeholder' }
image: { url: image3, alt: 'Image Placeholder' },
},
{
route: '/',
tooltip: '7 New Messages',
image: { url: image4, alt: 'Image Placeholder' }
}
image: { url: image4, alt: 'Image Placeholder' },
},
]"
/>
</div>
@ -170,18 +170,18 @@ export default {
EmailCard,
TodoCard,
MiniPlayerCard,
MessageCard
MessageCard,
},
data() {
return {
image1,
image2,
image3,
image4
image4,
};
},
computed: {
...mapState(["isTransparent", "isNavFixed", "navbarFixed", "mcolor"])
...mapState(["isTransparent", "isNavFixed", "navbarFixed", "mcolor"]),
},
mounted() {
setTooltip(this.$store.state.bootstrap);
@ -208,7 +208,7 @@ export default {
this.$store.state.isTransparent = "bg-transparent";
},
methods: {
...mapMutations(["navbarMinimize", "toggleConfigurator"])
}
...mapMutations(["navbarMinimize", "toggleConfigurator"]),
},
};
</script>

View File

@ -306,7 +306,7 @@ export default {
item = {
src: linkEl.getAttribute("href"),
w: parseInt(size[0], 10),
h: parseInt(size[1], 10)
h: parseInt(size[1], 10),
};
if (figureEl.children.length > 1) {
@ -430,9 +430,9 @@ export default {
return {
x: rect.left,
y: rect.top + pageYScroll,
w: rect.width
w: rect.width,
};
}
},
};
// PhotoSwipe opened from URL
@ -502,10 +502,10 @@ export default {
var element = document.getElementById(id);
return new Choices(element, {
searchEnabled: false,
itemSelectText: ""
itemSelectText: "",
});
}
}
}
},
},
};
</script>

View File

@ -916,7 +916,7 @@ export default {
const dataTableSearch = new DataTable("#products-list", {
searchable: true,
fixedHeight: false,
perPage: 7
perPage: 7,
});
document.querySelectorAll(".export").forEach(function (el) {
@ -925,7 +925,7 @@ export default {
var data = {
type: type,
filename: "soft-ui-" + type
filename: "soft-ui-" + type,
};
if (type === "csv") {
@ -937,6 +937,6 @@ export default {
});
}
setTooltip(this.$store.state.bootstrap);
}
},
};
</script>

View File

@ -7,22 +7,22 @@
:percentage="{
color: 'success',
value: '+55%',
label: 'since last month'
label: 'since last month',
}"
menu="6 May - 7 May"
:dropdown="[
{
label: 'Last 7 days',
route: 'https://creative-tim.com/'
route: 'https://creative-tim.com/',
},
{
label: 'Last week',
route: '/pages/widgets'
route: '/pages/widgets',
},
{
label: 'Last 30 days',
route: '/'
}
route: '/',
},
]"
/>
<default-statistics-card
@ -31,22 +31,22 @@
:percentage="{
color: 'success',
value: '+12%',
label: 'since last month'
label: 'since last month',
}"
menu="9 June - 12 June"
:dropdown="[
{
label: 'Last 7 days',
route: 'javascript:;'
route: 'javascript:;',
},
{
label: 'Last week',
route: 'javascript:;'
route: 'javascript:;',
},
{
label: 'Last 30 days',
route: 'javascript:;'
}
route: 'javascript:;',
},
]"
/>
<default-statistics-card
@ -55,22 +55,22 @@
:percentage="{
color: 'secondary',
value: '+$213',
label: 'since last month'
label: 'since last month',
}"
menu="6 August - 9 August"
:dropdown="[
{
label: 'Last 7 days',
route: 'javascript:;'
route: 'javascript:;',
},
{
label: 'Last week',
route: 'javascript:;'
route: 'javascript:;',
},
{
label: 'Last 30 days',
route: 'javascript:;'
}
route: 'javascript:;',
},
]"
/>
</div>
@ -97,9 +97,9 @@
datasets: [
{
label: 'Sales by age',
data: [15, 20, 12, 60, 20, 15]
}
]
data: [15, 20, 12, 60, 20, 15],
},
],
}"
/>
</div>
@ -180,7 +180,7 @@ export default {
RevenueChartCard,
HorizontalBarChart,
OrdersListCard,
DefaultStatisticsCard
DefaultStatisticsCard,
},
data() {
return {
@ -192,7 +192,7 @@ export default {
info: "Refund rate is lower with 97% than other products",
img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/blue-shoe.jpg",
icon: "bold-down text-success"
icon: "bold-down text-success",
},
{
title: "Business Kit (Mug + Notebook)",
@ -200,7 +200,7 @@ export default {
values: ["$80.250", "$4.200", "40"],
img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/black-mug.jpg",
icon: "bold-down text-success"
icon: "bold-down text-success",
},
{
title: "Black Chair",
@ -208,7 +208,7 @@ export default {
values: ["$40.600", "$9.430", "54"],
img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/black-chair.jpg",
icon: "bold-up text-danger"
icon: "bold-up text-danger",
},
{
title: "Wireless Charger",
@ -216,7 +216,7 @@ export default {
values: ["$91.300", "$7.364", "5"],
img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/bang-sound.jpg",
icon: "bold-down text-success"
icon: "bold-down text-success",
},
{
title: "Mountain Trip Kit (Camera + Backpack)",
@ -225,45 +225,45 @@ export default {
info: "Refund rate is higher with 70% than other products",
img:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/ecommerce/photo-tools.jpg",
icon: "bold-up text-danger"
}
icon: "bold-up text-danger",
},
],
sales: [
{
country: "United States",
sales: 2500,
bounce: "29.9%",
flag: US
flag: US,
},
{
country: "Germany",
sales: "3.900",
bounce: "40.22%",
flag: DE
flag: DE,
},
{
country: "Great Britain",
sales: "1.400",
bounce: "23.44%",
flag: GB
flag: GB,
},
{
country: "Brasil",
sales: "562",
bounce: "32.14%",
flag: BR
flag: BR,
},
{
country: "Australia",
sales: "400",
bounce: "56.83%",
flag: AU
}
]
flag: AU,
},
],
};
},
mounted() {
setTooltip(this.$store.state.bootstrap);
}
},
};
</script>

View File

@ -0,0 +1,149 @@
<template>
<div
class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg"
:style="{
backgroundImage:
'url(' + require('@/assets/img/curved-images/curved9.jpg') + ')',
}"
>
<span class="mask bg-gradient-dark opacity-6"></span>
<div class="container">
<div class="row justify-content-center">
<div class="mx-auto text-center col-lg-5">
<h1 class="mt-5 mb-2 text-white">Bonjour !</h1>
<p class="text-white text-lead">
Veuillez entrer vos identifiants pour vous connecter
</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row mt-lg-n10 mt-md-n11 mt-n10 justify-content-center">
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
<div class="card z-index-0">
<div class="pt-4 text-center card-header">
<h5>Connectez-vous</h5>
</div>
<div class="card-body">
<form role="form" class="text-start" @submit.prevent="handleLogin">
<div class="mb-3">
<SoftInput
id="email"
:value="email"
type="email"
placeholder="Email"
name="email"
:is-required="true"
@input="email = $event.target.value"
/>
</div>
<div class="mb-3">
<SoftInput
id="password"
:value="password"
name="password"
type="password"
placeholder="Mot de passe"
:is-required="true"
@input="password = $event.target.value"
/>
</div>
<SoftSwitch
id="rememberMe"
:checked="remember"
name="rememberMe"
@change="remember = $event.target.checked"
>
Souvenez de moi
</SoftSwitch>
<div
v-if="errorMessage"
class="alert alert-danger text-white"
role="alert"
>
{{ errorMessage }}
</div>
<div class="text-center">
<SoftButton
type="submit"
class="my-4 mb-2"
variant="gradient"
color="info"
full-width
:disabled="isLoading"
>
{{ isLoading ? "Connexion..." : "Se connecter" }}
</SoftButton>
</div>
<div class="mb-2 text-center position-relative">
<p
class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2"
>
ou
</p>
</div>
<div class="text-center">
<SoftButton
class="mt-2 mb-4"
variant="gradient"
color="dark"
full-width
@click="$router.push('/register')"
>
Creer un compte
</SoftButton>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import SoftInput from "@/components/SoftInput.vue";
import SoftSwitch from "@/components/SoftSwitch.vue";
import SoftButton from "@/components/SoftButton.vue";
const router = useRouter();
const authStore = useAuthStore();
const email = ref("");
const password = ref("");
const remember = ref(false);
const isLoading = ref(false);
const errorMessage = ref("");
const handleLogin = async () => {
if (!email.value || !password.value) {
errorMessage.value = "Veuillez remplir tous les champs";
return;
}
isLoading.value = true;
errorMessage.value = "";
try {
await authStore.login({
email: email.value,
password: password.value,
remember: remember.value,
});
// Redirect to dashboard on success
router.push("/dashboards/dashboard-default");
} catch (error: any) {
console.error("Login error:", error);
errorMessage.value =
error.response?.data?.message ||
error.message ||
"Email ou mot de passe incorrect";
} finally {
isLoading.value = false;
}
};
</script>

View File

@ -0,0 +1,153 @@
<template>
<div
class="pt-5 m-3 page-header align-items-start min-vh-50 pb-11 border-radius-lg"
:style="{
backgroundImage:
'url(' + require('@/assets/img/curved-images/curved9.jpg') + ')',
}"
>
<span class="mask bg-gradient-dark opacity-6"></span>
<div class="container">
<div class="row justify-content-center">
<div class="mx-auto text-center col-lg-5">
<h1 class="mt-5 mb-2 text-white">Bienvenue !</h1>
<p class="text-white text-lead">Créez votre compte pour commencer</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row mt-lg-n10 mt-md-n11 mt-n10 justify-content-center">
<div class="mx-auto col-xl-4 col-lg-5 col-md-7">
<div class="card z-index-0">
<div class="pt-4 text-center card-header">
<h5>Créer un compte</h5>
</div>
<div class="card-body">
<form
role="form"
class="text-start"
@submit.prevent="handleRegister"
>
<div class="mb-3">
<SoftInput
id="name"
:value="name"
type="text"
placeholder="Nom"
name="name"
:is-required="true"
@input="name = $event.target.value"
/>
</div>
<div class="mb-3">
<SoftInput
id="email"
:value="email"
type="email"
placeholder="Email"
name="email"
:is-required="true"
@input="email = $event.target.value"
/>
</div>
<div class="mb-3">
<SoftInput
id="password"
:value="password"
name="password"
type="password"
placeholder="Mot de passe"
:is-required="true"
@input="password = $event.target.value"
/>
</div>
<div
v-if="errorMessage"
class="alert alert-danger text-white"
role="alert"
>
{{ errorMessage }}
</div>
<div class="text-center">
<SoftButton
type="submit"
class="my-4 mb-2"
variant="gradient"
color="success"
full-width
:disabled="isLoading"
>
{{ isLoading ? "Inscription..." : "Creer un compte" }}
</SoftButton>
</div>
<div class="mb-2 text-center position-relative">
<p
class="px-3 mb-2 text-sm bg-white font-weight-bold text-secondary text-border d-inline z-index-2"
>
ou
</p>
</div>
<div class="text-center">
<SoftButton
class="mt-2 mb-4"
variant="gradient"
color="dark"
full-width
@click="$router.push('/login')"
>
Se connecter
</SoftButton>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue";
const router = useRouter();
const authStore = useAuthStore();
const name = ref("");
const email = ref("");
const password = ref("");
const isLoading = ref(false);
const errorMessage = ref("");
const handleRegister = async () => {
if (!name.value || !email.value || !password.value) {
errorMessage.value = "Veuillez remplir tous les champs";
return;
}
isLoading.value = true;
errorMessage.value = "";
try {
await authStore.register({
name: name.value,
email: email.value,
password: password.value,
});
// Redirect to dashboard on success (user is automatically logged in)
router.push("/dashboards/dashboard-default");
} catch (error: any) {
console.error("Registration error:", error);
errorMessage.value =
error.response?.data?.message ||
error.message ||
"Erreur lors de l'inscription";
} finally {
isLoading.value = false;
}
};
</script>

View File

@ -5,7 +5,7 @@
:style="{
backgroundImage:
'url(' + require('@/assets/img/curved-images/curved14.jpg') + ')',
backgroundPositionY: '50%'
backgroundPositionY: '50%',
}"
>
<span class="mask bg-gradient-success opacity-6"></span>
@ -283,25 +283,25 @@
fullName: 'Alec M. Thompson',
mobile: '(44) 123 1234 123',
email: 'alecthompson@mail.com',
location: 'USA'
location: 'USA',
}"
:social="[
{
link: 'https://www.facebook.com/CreativeTim/',
icon: faFacebook
icon: faFacebook,
},
{
link: 'https://twitter.com/creativetim',
icon: faTwitter
icon: faTwitter,
},
{
link: 'https://www.instagram.com/creativetimofficial/',
icon: faInstagram
}
icon: faInstagram,
},
]"
:action="{
route: 'javascript:;',
tooltip: 'Edit Profile'
tooltip: 'Edit Profile',
}"
/>
</div>
@ -445,24 +445,24 @@
:authors="[
{
image: team1,
name: 'Elena Morison'
name: 'Elena Morison',
},
{
image: team2,
name: 'Ryan Milly'
name: 'Ryan Milly',
},
{
image: team3,
name: 'Nick Daniel'
name: 'Nick Daniel',
},
{
image: team4,
name: 'Peterson'
}
name: 'Peterson',
},
]"
:action="{
color: 'success',
label: 'View Project'
label: 'View Project',
}"
/>
@ -475,24 +475,24 @@
:authors="[
{
image: team3,
name: 'Nick Daniel'
name: 'Nick Daniel',
},
{
image: team4,
name: 'Peterson'
name: 'Peterson',
},
{
image: team1,
name: 'Elena Morison'
name: 'Elena Morison',
},
{
image: team2,
name: 'Ryan Milly'
}
name: 'Ryan Milly',
},
]"
:action="{
color: 'success',
label: 'View Project'
label: 'View Project',
}"
/>
@ -505,24 +505,24 @@
:authors="[
{
image: team4,
name: 'Peterson'
name: 'Peterson',
},
{
image: team3,
name: 'Nick Daniel'
name: 'Nick Daniel',
},
{
image: team1,
name: 'Elena Morison'
name: 'Elena Morison',
},
{
image: team2,
name: 'Ryan Milly'
}
name: 'Ryan Milly',
},
]"
:action="{
color: 'success',
label: 'View Project'
label: 'View Project',
}"
/>
@ -558,7 +558,7 @@ import team4 from "@/assets/img/team-4.jpg";
import {
faFacebook,
faTwitter,
faInstagram
faInstagram,
} from "@fortawesome/free-brands-svg-icons";
import DefaultProjectCard from "./components/DefaultProjectCard.vue";
import PlaceHolderCard from "@/examples/Cards/PlaceHolderCard.vue";
@ -572,7 +572,7 @@ export default {
ProfileInfoCard,
SoftAvatar,
DefaultProjectCard,
PlaceHolderCard
PlaceHolderCard,
},
data() {
return {
@ -591,7 +591,7 @@ export default {
img3,
faFacebook,
faTwitter,
faInstagram
faInstagram,
};
},
@ -602,6 +602,6 @@ export default {
},
beforeUnmount() {
this.$store.state.isAbsolute = false;
}
},
};
</script>

View File

@ -5,7 +5,7 @@
:style="{
backgroundImage:
'url(' + require('@/assets/img/curved-images/curved14.jpg') + ')',
backgroundPositionY: '50%'
backgroundPositionY: '50%',
}"
>
<span class="mask bg-gradient-success opacity-6"></span>
@ -232,34 +232,34 @@
:members="[
{
name: 'Alexa Tompson',
image: team1
image: team1,
},
{
name: 'Romina Hadid',
image: team2
image: team2,
},
{
name: 'Alexander Smith',
image: team3
image: team3,
},
{
name: 'Martin Doe',
image: team4
}
image: team4,
},
]"
:dropdown="[
{
label: 'Action',
route: 'javascript:;'
route: 'javascript:;',
},
{
label: 'Another action',
route: 'javascript:;'
route: 'javascript:;',
},
{
label: 'Something else here',
route: 'javascript:;'
}
route: 'javascript:;',
},
]"
/>
<team-profile-card
@ -272,34 +272,34 @@
:members="[
{
name: 'Martin Doe',
image: team4
image: team4,
},
{
name: 'Alexander Smith',
image: team3
image: team3,
},
{
name: 'Romina Hadid',
image: team2
image: team2,
},
{
name: 'Alexa Tompson',
image: team1
}
image: team1,
},
]"
:dropdown="[
{
label: 'Action',
route: 'javascript:;'
route: 'javascript:;',
},
{
label: 'Another action',
route: 'javascript:;'
route: 'javascript:;',
},
{
label: 'Something else here',
route: 'javascript:;'
}
route: 'javascript:;',
},
]"
/>
<event-card
@ -312,12 +312,12 @@
{ name: 'Alexa tompson', image: team1 },
{ name: 'Romina Hadid', image: team2 },
{ name: 'Alexander Smith', image: team3 },
{ name: 'Martin Doe', image: ivana }
{ name: 'Martin Doe', image: ivana },
]"
:action="{
route: '',
label: 'Join',
color: 'success'
color: 'success',
}"
/>
<event-card
@ -331,12 +331,12 @@
{ name: 'Alexa tompson', image: team1 },
{ name: 'Romina Hadid', image: team2 },
{ name: 'Alexander Smith', image: team3 },
{ name: 'Martin Doe', image: ivana }
{ name: 'Martin Doe', image: ivana },
]"
:action="{
route: '',
label: 'Join',
color: 'primary'
color: 'primary',
}"
/>
</div>
@ -372,7 +372,7 @@ export default {
TeamProfileCard,
PostCard,
StoryAvatar,
EventCard
EventCard,
},
data() {
return {
@ -380,49 +380,49 @@ export default {
stories: [
{
name: "Abbie W",
image: team1
image: team1,
},
{
name: "Boris U",
image: team2
image: team2,
},
{
name: "Kay R",
image: team3
image: team3,
},
{
name: "Tom M",
image: team4
image: team4,
},
{
name: "Nicole N",
image: team5
image: team5,
},
{
name: "Marie P",
image: marie
image: marie,
},
{
name: "Bruce M",
image: bruce
image: bruce,
},
{
name: "Sandra A",
image: ivana
image: ivana,
},
{
name: "Katty L",
image: kal
image: kal,
},
{
name: "Emma O",
image: emma
image: emma,
},
{
name: "Tao G",
image:
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/team-9.jpg"
}
"https://raw.githubusercontent.com/creativetimofficial/public-assets/master/soft-ui-design-system/assets/img/team-9.jpg",
},
],
nick,
slackLogo,
@ -436,7 +436,7 @@ export default {
team2,
team3,
team4,
team5
team5,
};
},
@ -447,6 +447,6 @@ export default {
},
beforeUnmount() {
this.$store.state.isAbsolute = false;
}
},
};
</script>

View File

@ -1,30 +1,40 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"allowJs": true,
"checkJs": false,
"noEmit": true,
"strict": false,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": ["src/*"]
"@/*": [
"src/*"
]
},
"types": ["node"]
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/shims-vue.d.ts"
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": ["node_modules", "dist"]
"exclude": [
"node_modules"
]
}

View File

@ -1,24 +0,0 @@
const path = require("path");
module.exports = {
// Dossier de sortie du build
outputDir: path.resolve(__dirname, "../thanasoft-back/public/build"),
// Le index.html généré
indexPath: "index.html",
// En prod on sert les assets depuis /build/, en dev on reste sur la racine
publicPath: process.env.NODE_ENV === "production" ? "/build/" : "/",
// Dev server config to work locally alongside Laravel backend
devServer: {
historyApiFallback: true,
// proxy API calls to Laravel (adjust if your API prefix differs)
proxy: {
"^/api": {
target: "http://127.0.0.1:8000",
changeOrigin: true,
},
},
},
};