feat: userProfile

This commit is contained in:
Michael 2025-07-18 14:10:37 +02:00
parent 8f24453eff
commit 13a3de9709
16 changed files with 492 additions and 284 deletions

View file

@ -26,7 +26,7 @@ class UpdateUserPassword implements UpdatesUserPasswords
])->validateWithBag('updatePassword'); ])->validateWithBag('updatePassword');
$user->forceFill([ $user->forceFill([
'password' => Hash::make($input['password']), 'pass' => Hash::make($input['password']),
])->save(); ])->save();
} }
} }

View file

@ -13,43 +13,48 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
/** /**
* Validate and update the given user's profile information. * Validate and update the given user's profile information.
* *
* @param array<string, string> $input * @param array<string, mixed> $input
*/ */
public function update(User $user, array $input): void public function update(User $user, array $input): void
{ {
// Use PHP's built-in list of timezones for robust validation
$timezones = timezone_identifiers_list();
$rules = [ $rules = [
'login' => ['required', 'string', 'max:255', Rule::unique('cc_subjs')->ignore($user->id)], 'login' => ['required', 'string', 'max:255', Rule::unique('cc_subjs')->ignore($user->id)],
'email' => [ 'email' => ['nullable', 'string', 'email', 'max:255', Rule::unique('cc_subjs')->ignore($user->id)],
'required', 'first_name' => ['nullable', 'string', 'max:255'],
'string', 'last_name' => ['nullable', 'string', 'max:255'],
'email', 'cell_phone' => ['nullable', 'string', 'max:25'],
'max:255', 'timezone' => ['nullable', 'string', Rule::in($timezones)],
Rule::unique('cc_subjs')->ignore($user->id),
],
]; ];
// Only add the 'type' validation rule if the user has the permission to change roles if (isset($rules['type']) && auth()->user()->hasPermissionTo('users.changeRole')) {
if (auth()->user()->hasPermissionTo('users.changeRole')) {
$rules['type'] = ['required', 'string', 'max:6', Rule::in(['admin', 'editor', 'dj'])]; $rules['type'] = ['required', 'string', 'max:6', Rule::in(['admin', 'editor', 'dj'])];
} }
Validator::make($input, $rules)->validateWithBag('updateProfileInformation'); Validator::make($input, $rules)->validateWithBag('updateProfileInformation');
$data = [
'login' => $input['login'],
'email' => $input['email'],
'first_name' => $input['first_name'],
'last_name' => $input['last_name'],
'cell_phone' => $input['cell_phone'],
];
if ($input['email'] !== $user->email && $user instanceof MustVerifyEmail) { if ($input['email'] !== $user->email && $user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input); $this->updateVerifiedUser($user, $input);
} else { } else {
$data = [ if (isset($rules['type']) && auth()->user()->hasPermissionTo('users.changeRole')) {
'login' => $input['login'],
'email' => $input['email'],
];
// Only update 'type' if the user has the permission
if (auth()->user()->hasPermissionTo('users.changeRole')) {
$data['type'] = $input['type']; $data['type'] = $input['type'];
} }
$user->forceFill($data)->save(); $user->forceFill($data)->save();
} }
// The timezone is handled by the mutator in the User model
$user->timezone = $input['timezone'];
} }
/** /**
@ -62,16 +67,22 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
$data = [ $data = [
'login' => $input['login'], 'login' => $input['login'],
'email' => $input['email'], 'email' => $input['email'],
'first_name' => $input['first_name'],
'last_name' => $input['last_name'],
'cell_phone' => $input['cell_phone'],
'email_verified_at' => null, 'email_verified_at' => null,
]; ];
// Only update 'type' if the user has the permission // Corrected permission name to be consistent
if (auth()->user()->hasPermissionTo('user.changeRole')) { if (auth()->user()->hasPermissionTo('users.changeRole')) {
$data['type'] = $input['type']; $data['type'] = $input['type'];
} }
$user->forceFill($data)->save(); $user->forceFill($data)->save();
// The timezone is handled by the mutator in the User model
$user->timezone = $input['timezone'];
$user->sendEmailVerificationNotification(); $user->sendEmailVerificationNotification();
} }
} }

View file

@ -56,34 +56,38 @@ class UserController extends Controller
public function userProfile() public function userProfile()
{ {
return response()->json(auth()->user()); $user =auth()->user();
$user->role = $user->roles()->value('name');
return response()->json($user);
} }
public function update(Request $request, User $user, UpdateUserProfileInformation $updater) public function update(Request $request, User $user, UpdateUserProfileInformation $updater)
{ {
$authenticatedUser = auth()->user(); $authenticatedUser = auth()->user();
if ($authenticatedUser->id !== $user->id && ! $authenticatedUser->hasPermissionTo('user.manageAll')) { if ($authenticatedUser->id !== $user->id && !$authenticatedUser->hasPermissionTo('user.manageAll')) {
return response()->json(['message' => 'You do not have permission to edit other users.'], 403); return response()->json(['message' => 'You do not have permission to edit other users.'], 403);
} }
if ($authenticatedUser->id === $user->id && ! $authenticatedUser->hasPermissionTo('users.manageOwn')) { if ($authenticatedUser->id === $user->id && !$authenticatedUser->hasPermissionTo('users.manageOwn')) {
return response()->json(['message' => 'You do not have permission to edit your own profile.'], 403); return response()->json(['message' => 'You do not have permission to edit your own profile.'], 403);
} }
try { try {
(new UpdateUserProfileInformation())->update($user, $request->all()); $updater->update($user, $request->all());
$user->load('preferences'); $user->load('preferences');
return response()->json($user); return response()->json($user);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error($e->getMessage()); Log::error($e->getMessage());
if ($e instanceof \Illuminate\Validation\ValidationException) {
return response()->json(['message' => $e->getMessage(), 'errors' => $e->errors()], 422);
}
return response()->json(['message' => 'Failed to update user'], 500); return response()->json(['message' => 'Failed to update user'], 500);
} }
} }
public function destroy(Request $request) public function destroy(Request $request)
{ {
try { try {

View file

@ -8,6 +8,8 @@ import { createPinia } from "pinia";
import { createI18n } from "vue-i18n"; import { createI18n } from "vue-i18n";
import App from "@/layouts/App.vue"; import App from "@/layouts/App.vue";
import { useAuthStore } from '@/stores/auth.store';
const pinia = createPinia(); const pinia = createPinia();
const i18n = createI18n(vueI18n); const i18n = createI18n(vueI18n);
const app = createApp(App); const app = createApp(App);
@ -16,4 +18,8 @@ app.use(pinia)
.use(i18n) .use(i18n)
.use(router) .use(router)
.use(vuetify) .use(vuetify)
.mount("#app");
const auth = useAuthStore();
auth.fetchUser().finally(() => {
app.mount("#app");
});

View file

@ -6,7 +6,7 @@
import axios from 'axios'; import axios from 'axios';
window.axios = axios; window.axios = axios;
window.axios.defaults.withCredentials = true
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/** /**

View file

@ -91,29 +91,31 @@ onDeactivated(() => {
</script> </script>
<template> <template>
<template v-if="showCreateEditMode || showInstanceCreateEditMode"> <div>
<ShowForm v-if="showCreateEditMode" :showId="showSelected" @go-back="goBack"/> <template v-if="showCreateEditMode || showInstanceCreateEditMode">
<ShowInstanceForm v-if="showInstanceCreateEditMode" :showInstance="selectedShowInstance" <ShowForm v-if="showCreateEditMode" :showId="showSelected" @go-back="goBack"/>
@toggle-menu-edit-instance="goBack"/> <ShowInstanceForm v-if="showInstanceCreateEditMode" :showInstance="selectedShowInstance"
</template> @toggle-menu-edit-instance="goBack"/>
<template v-else> </template>
<v-row class="ma-4 justify-space-around"> <template v-else>
<template v-if="userRole && (userRole == 'admin' || userRole == 'editor')"> <v-row class="ma-4 justify-space-around">
<v-btn color="primary" @click="createShow">Crea show</v-btn> <template v-if="userRole && (userRole == 'admin' || userRole == 'editor')">
<v-btn color="secondary" @click="toggleEditMode"> <v-btn color="primary" @click="createShow">Crea show</v-btn>
{{ editMode ? 'Disabilita modifica calendario' : 'Abilita modifica calendario' }} <v-btn color="secondary" @click="toggleEditMode">
</v-btn> {{ editMode ? 'Disabilita modifica calendario' : 'Abilita modifica calendario' }}
</template> </v-btn>
</v-row> </template>
<CalendarShowEvent </v-row>
:edit-mode="editMode" <CalendarShowEvent
:shows="shows" :edit-mode="editMode"
@contextMenuEditInstance="contextMenuEditInstance" :shows="shows"
@contextMenuEditShow="contextMenuEditShow" @contextMenuEditInstance="contextMenuEditInstance"
@contextMenuDeleteInstance="contextMenuDeleteInstance" @contextMenuEditShow="contextMenuEditShow"
@contextMenuDeleteShow="contextMenuDeleteShow" @contextMenuDeleteInstance="contextMenuDeleteInstance"
/> @contextMenuDeleteShow="contextMenuDeleteShow"
</template> />
</template>
</div>
</template> </template>
<style scoped> <style scoped>

View file

@ -1,91 +1,233 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAuthStore } from '@/stores/auth.store.ts'; // Adjust the import path to your store import {useAuthStore} from '@/stores/auth.store.ts';
import { storeToRefs } from 'pinia'; import {storeToRefs} from 'pinia';
import { ref } from 'vue'; import {onBeforeMount, ref, reactive} from 'vue';
import {useRouter} from "vue-router";
import axios from "axios";
const router = useRouter();
const authStore = useAuthStore(); const authStore = useAuthStore();
const { userData } = storeToRefs(authStore); const {userData} = storeToRefs(authStore);
const localUserData = reactive({...userData.value});
const emit = defineEmits([ const emit = defineEmits([
'userProfilePage' 'userProfilePage'
]) ]);
// Sample data for selection components let timezones = ref<string[]>([]);
const roles = ['Admin', 'User', 'Editor']; let roleList = ref<string[]>([]);
const timezones = [
'UTC',
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'Europe/London',
'Europe/Berlin',
'Asia/Tokyo',
];
const form = ref(null); const form = ref<HTMLFormElement | null>(null);
const passwordForm = ref<HTMLFormElement | null>(null);
const dialog = ref(false);
const passwordData = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
const formRules = {
'emailRules': [(v: string) => !v || /.+@.+\..+/.test(v) || 'E-mail must be a valid format'],
'cellphoneRules': [(v: string) => !v || /^[0-9-()]*$/.test(v) || 'Cellphone must be a valid number'],
'passwordConfirmationRule': [
(v: string) => !!v || 'Password confirmation is required',
(v: string) => v === passwordData.newPassword || 'Passwords do not match'
],
'requiredRule': [(v: string) => !!v || 'This field is required'],
}
const saveUser = async () => { const saveUser = async () => {
if (!form.value) return
const {valid} = await form.value.validate();
if (!valid) return
authStore.userData = {...localUserData};
await authStore.updateUser(); await authStore.updateUser();
}; };
const goBack = () => {
router.go(-1);
}
const openPasswordDialog = () => {
dialog.value = true;
};
const closePasswordDialog = () => {
dialog.value = false;
passwordForm.value?.reset();
passwordForm.value?.resetValidation();
};
const resetPassword = async () => {
console.log('aaaa')
try {
await axios.put('/api/user/password', passwordData)
console.log('Password changed');
closePasswordDialog()
return
} catch (e) {
const errorMessage = e.response?.data?.error || 'An unexpected error occurred.';
console.error('Error changing password:' + errorMessage);
return
}
};
onBeforeMount(async () => {
await axios.get('/timezoneList').then(response => {
timezones.value = response?.data
})
if (userData.value.role === 'admin') {
await axios.get('/roleList').then(response => {
roleList.value = response?.data
})
}
})
</script> </script>
<template> <template>
<v-form ref="form"> <div>
<v-container> <v-form ref="form">
<v-row> <v-container>
<v-col cols="12" md="6"> <v-row>
<v-text-field <v-col cols="12" md="6">
v-model="userData.login" <v-text-field
label="Login" v-model="localUserData.login"
required label="Login"
></v-text-field> required
</v-col> hint="Your public username."
persistent-hint
></v-text-field>
</v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="userData.firstName" v-model="localUserData.email"
label="First Name" :rules="formRules.emailRules"
></v-text-field> label="Email Address"
</v-col> hint="Used for notifications and account recovery."
persistent-hint
></v-text-field>
</v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="userData.lastName" v-model="localUserData.firstName"
label="Last Name" label="First Name"
></v-text-field> required
</v-col> ></v-text-field>
</v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-autocomplete <v-text-field
v-model="userData.timezone" v-model="localUserData.lastName"
:items="timezones" label="Last Name"
label="Timezone" required
></v-autocomplete> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="userData.email" v-model="localUserData.cellPhone"
label="email" :rules="formRules.cellphoneRules"
></v-text-field> label="Cell Phone"
</v-col> ></v-text-field>
</v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-autocomplete
v-model="userData.cellPhone" v-model="localUserData.timezone"
label="cellPhone" :items="timezones"
></v-text-field> label="Timezone"
</v-col> hint="Sets the time for all events and schedules."
</v-row> persistent-hint
<v-row> ></v-autocomplete>
<v-col> </v-col>
<v-btn color="primary" @click="saveUser">
Salva <v-col cols="12" md="6">
</v-btn> <v-select
</v-col> v-model="localUserData.role"
</v-row> :items="roleList"
</v-container> label="Ruolo"
</v-form> :disabled="userData.role !== 'admin'"
></v-select>
</v-col>
</v-row>
<v-row>
<v-col class="d-flex justify-end">
<v-btn color="secondary" @click="goBack" class="mr-4">
Back
</v-btn>
<v-btn color="primary" @click="saveUser">
Save Changes
</v-btn>
<v-btn color="error" @click="openPasswordDialog">
Reset Password
</v-btn>
</v-col>
</v-row>
</v-container>
</v-form>
<v-dialog v-model="dialog" persistent max-width="600px">
<v-card>
<v-card-title>
<span class="text-h5">Reset Your Password</span>
</v-card-title>
<v-card-text>
<p class="text-subtitle-1 mb-4">
Please be sure about the new password you are choosing. Password recovery via email is not implemented yet,
so
a forgotten password cannot be recovered.
</p>
</v-card-text>
<v-form ref="passwordForm" @submit.prevent="resetPassword">
<v-container>
<v-row>
<v-col cols="12">
<v-text-field
v-model="passwordData.oldPassword"
label="Old Password"
type="password"
:rules="formRules.requiredRule"
required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
v-model="passwordData.newPassword"
label="New Password"
type="password"
:rules="formRules.requiredRule"
required
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
v-model="passwordData.confirmPassword"
label="Confirm New Password"
type="password"
:rules="formRules.passwordConfirmationRule"
required
></v-text-field>
</v-col>
</v-row>
</v-container>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="closePasswordDialog">
Cancel
</v-btn>
<v-btn type="submit" color="blue-darken-1" variant="tonal">
Confirm
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</div>
</template> </template>

View file

@ -7,21 +7,9 @@ const router = useRouter();
const auth = useAuthStore(); const auth = useAuthStore();
const userName = auth.userData.login; const userName = auth.userData.login;
const emit = defineEmits([
'userProfilePage'
])
const userProfilePage = () => {
emit('userProfilePage')
}
const logout = async () => { const logout = async () => {
try { await auth.logout()
await axios.get('/logout'); router.push({ path: 'login' });
auth.resetUser();
router.push('/login');
} catch (error) {
console.error('Error logging out:', error);
}
}; };
</script> </script>
@ -30,7 +18,7 @@ const logout = async () => {
<v-sheet <v-sheet
:width="150" :width="150"
> >
<v-btn color="info" @click="userProfilePage">{{ userName }} {{ $t('header.userinfo.info') }}</v-btn> <v-btn color="info" :to="{ path: 'user-profile'}">{{ userName }} {{ $t('header.userinfo.info') }}</v-btn>
<v-btn color="" @click="logout">{{ $t('header.userinfo.logout') }}</v-btn> <v-btn color="" @click="logout">{{ $t('header.userinfo.logout') }}</v-btn>
</v-sheet> </v-sheet>
</template> </template>

View file

@ -1,11 +1,74 @@
import { createRouter, createWebHistory } from "vue-router"; import {createRouter, createWebHashHistory, createWebHistory, type RouteRecordRaw} from "vue-router";
import { useAuthStore } from "@/stores/auth.store.ts"; import {useAuthStore} from "@/stores/auth.store.ts";
import {useShowTypeStore} from '@/stores/showType.store';
const routes = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/', path: '/',
name: 'Dashboard', name: 'Backoffice',
component: () => import('../pages/Backoffice.vue'), component: () => import('../pages/Backoffice.vue'),
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/components/content/Dashboard.vue')
},
{
path: 'show',
name: 'Trasmissioni',
component: () => import('@/components/content/Show.vue'),
meta: {showType: 'shows'}
},
{
path: 'archive',
name: 'Archivio',
component: () => import('@/components/content/Archive.vue')},
{
path: 'playlist',
name: 'Playlist',
component: () => import('@/components/content/Playlist.vue'),
meta: {showType: 'shows'}
},
{
path: 'blocks',
name: 'Blocchi dinamici',
component: () => import('@/components/content/SmartBlock.vue'),
meta: {showType: 'shows'}
},
{
path: 'podcast',
name: 'Podcast',
component: () => import('@/components/content/Podcast.vue')
},
{
path: 'webstream',
name: 'Webstream',
component: () => import('@/components/content/Webstream.vue')
},
{
path: 'spot',
name: 'Spot',
component: () => import('@/components/content/Show.vue'),
meta: {showType: 'spots'}
},
{
path: 'spot-playlist',
name: 'Spot playlist',
component: () => import('@/components/content/Playlist.vue'),
meta: {showType: 'spots'}
},
{
path: 'spot-blocks',
name: 'Spot Blocchi dinamici',
component: () => import('@/components/content/SmartBlock.vue'),
meta: {showType: 'spots'}
},
{
path: 'user-profile',
name: 'UserProfile',
component: () => import('@/components/content/UserProfile.vue')
},
]
}, },
{ {
path: '/login', path: '/login',
@ -15,22 +78,35 @@ const routes = [
]; ];
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHashHistory(),
routes routes
}); });
export default router;
/** /**
* Redirect to login page if unauthenticated * Navigation Guards
*/ */
router.beforeEach(async (to) => { router.beforeEach(async (to, from, next) => {
const publicPages = ['/login']; const publicPages = ['/login'];
const authRequired = !publicPages.includes(to.path); const authRequired = !publicPages.includes(to.path);
const auth = useAuthStore(); const auth = useAuthStore();
if (authRequired && !auth.userData.login) { if (authRequired && !auth.userData.login) {
return '/login'; return next('/login');
} }
const showTypeStore = useShowTypeStore();
switch (to.meta.showType) {
case 'shows':
showTypeStore.setAsShows();
break;
case 'spots':
showTypeStore.setAsSpots();
break;
default:
showTypeStore.clearType();
}
next();
}); });
export default router;

View file

@ -46,6 +46,7 @@ watch(currentPage, (newVal) => {
<template> <template>
<v-col> <v-col>
<router-view></router-view>
<!-- <keep-alive>--> <!-- <keep-alive>-->
<Component :is="tabs[currentPage]" /> <Component :is="tabs[currentPage]" />
<!-- </keep-alive>--> <!-- </keep-alive>-->

View file

@ -3,14 +3,7 @@ import OnAir from "@/components/header/OnAir.vue";
import Clock from "@/components/header/Clock.vue"; import Clock from "@/components/header/Clock.vue";
import Timer from "@/components/header/Timer.vue"; import Timer from "@/components/header/Timer.vue";
import UserInfo from "@/components/header/UserInfo.vue"; import UserInfo from "@/components/header/UserInfo.vue";
import {useRouter} from "vue-router";
const emit = defineEmits([
'userProfilePage'
])
const userProfilePage = () => {
emit('userProfilePage')
}
</script> </script>
<template> <template>
@ -18,7 +11,7 @@ const userProfilePage = () => {
<OnAir /> <OnAir />
<Clock /> <Clock />
<Timer /> <Timer />
<UserInfo @user-profile-page="userProfilePage" /> <UserInfo />
</header> </header>
</template> </template>

View file

@ -1,66 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
const pages = [ import { useRouter } from 'vue-router';
{ import {useAuthStore} from "@stores/auth.store.ts";
id: 'dashboard', const auth = useAuthStore();
name: 'Dashboard', const router = useRouter();
component: '@components/content/Dashboard.vue', const backofficeRoutes = router.options.routes.find(route => route.name === 'Backoffice')?.children || [];
}, const pages = backofficeRoutes.filter(route => {
{ if (route.name === 'UserProfile') {
id: 'show', return false;
name: 'Trasmissioni', }
component: '@components/content/Show.vue', if (auth.currentUser.role !== 'admin' && route.name === 'adminDashboard') {
}, return false;
{ }
id: 'archive', if (!['admin', 'editor'].includes(auth.currentUser.role) && route?.meta?.showType === 'spots') {
name: 'Archivio', return false;
component: '@components/content/Archive.vue', }
}, return true;
{ });
id: 'playlist',
name: 'Playlist',
component: '@components/content/Playlist.vue',
},
{
id: 'blocks',
name: 'Blocchi dinamici',
component: '@components/content/SmartBlock.vue',
},
{
id: 'podcast',
name: 'Podcast',
component: '@components/content/Podcast.vue',
},
{
id: 'webstream',
name: 'Webstream',
component: '@components/content/Webstream.vue',
},
{
id: 'spot',
name: 'Spot',
component: '@components/content/Show.vue',
},
{
id: 'spot-playlist',
name: 'Spot playlist',
component: '@components/content/Playlist.vue',
},
{
id: 'spot-blocks',
name: 'Spot Blocchi dinamici',
component: '@components/content/SmartBlock.vue',
},
];
</script> </script>
<template> <template>
<v-col> <v-col>
<v-btn <v-btn
v-for="page in pages" v-for="page in pages"
:key="page.id" :key="page.path"
:to="{ name: page.name }"
size="large" size="large"
@click="$emit('showPage', page)" exact
>{{ page.name }}</v-btn> >{{ page.name }}</v-btn>
</v-col> </v-col>
</template> </template>

View file

@ -1,31 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import {useAuthStore} from "@/stores/auth.store.ts";
import Header from "@/layouts/partials/Header.vue"; import Header from "@/layouts/partials/Header.vue";
import Sidebar from "@/layouts/partials/Sidebar.vue"; import Sidebar from "@/layouts/partials/Sidebar.vue";
import Content from "@/layouts/partials/Content.vue"; import { useRouter } from "vue-router";
import {reactive} from "vue";
const page = reactive({ const router = useRouter();
id: 'dashboard',
name: 'Dashboard',
})
let returnToPage = {id: '', name: ''}
const userProfilePage = () => { const userProfilePage = () => {
if(page.id == 'userProfile') { router.push({ name: 'UserProfile' });
page.id = returnToPage.id
page.name = returnToPage.name
return
}
returnToPage = {...page}
page.id = 'userProfile'
page.name = 'userProfile'
}
const changePage = (currentPage) => {
page.id = currentPage.id;
page.name = currentPage.name;
} }
</script> </script>
@ -33,17 +14,17 @@ const changePage = (currentPage) => {
<div> <div>
<Header @user-profile-page="userProfilePage" /> <Header @user-profile-page="userProfilePage" />
<v-row :fluid="true"> <v-row :fluid="true">
<Sidebar <Sidebar />
@show-page="changePage" <router-view class="routed-component"/>
/>
<Content
:page="page"
/>
</v-row> </v-row>
</div> </div>
</template> </template>
<style scoped> <style scoped>
div {
flex-wrap: nowrap;
}
.v-row { .v-row {
margin: 0; margin: 0;
} }

View file

@ -1,12 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import {onBeforeMount, reactive} from 'vue';
import axios from "axios"; import axios from "axios";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {Settings} from "luxon"; import {onBeforeMount, reactive} from 'vue';
import {useAuthStore} from "@/stores/auth.store.ts"; import {useAuthStore} from "@/stores/auth.store.ts";
axios.defaults.withCredentials = true
const data = reactive({ const data = reactive({
'username': null, 'username': null,
'password': null, 'password': null,
@ -17,56 +14,29 @@ const auth = useAuthStore();
const router = useRouter(); const router = useRouter();
const onSubmit = async () => { const onSubmit = async () => {
let loginSuccessful = false
if (!data.username || !data.password) return; if (!data.username || !data.password) return;
data.loading = true; data.loading = true;
await axios.get('/sanctum/csrf-cookie').then(() => { const response = await auth.login(data.username, data.password);
axios.post('/login', { if (response?.error) {
username: data.username, data.errors = response?.error
password: data.password, data.loading = false;
}).then(async (response) => { return
let timezone = await getTimezone(); }
if(response.data.timezone) {
timezone = response.data.timezone
} else {
response.data.timezone = timezone
}
auth.loadUser(response.data); data.loading = false;
Settings.defaultZone = timezone; await router.push('/');
try {
await router.push('/');
} catch (e) {
console.error(e)
}
}).catch((error) => {
data.errors = error.response.data.errors
data.loading = false;
});
})
} }
const getTimezone = (): Promise<string> => {
return axios.get("timezone").then((res) => {
return res.data;
}).catch(error => {
return null;
})
}
const required = (v) => { const required = (v) => {
return !!v || $t('login.errors.required'); return !!v || $t('login.errors.required');
} }
onBeforeMount(() => { onBeforeMount(()=>{
// TODO Create a route taht checks if the user is already logged in (laravel session cookie), if it's logged in if(auth.isAuthenticated){
// route.push / router.push('/')
// In the BE make something like route.get('userIsAuthenticated')->return auth()->user() }
}) })
</script> </script>
<template> <template>

View file

@ -1,7 +1,8 @@
import {defineStore} from 'pinia' import {defineStore} from 'pinia'
import type {User} from "@models/User.ts"; import type {User} from "@models/User.ts";
import axios, {type AxiosResponse} from "axios"; import axios, {type AxiosResponse} from "axios";
import {camelToSnake} from "@/helpers/AxiosHelper.ts"; import {camelToSnake, snakeToCamel} from "@/helpers/AxiosHelper.ts";
import {Settings} from "luxon";
export const baseUser = (): User => ({ export const baseUser = (): User => ({
@ -15,7 +16,12 @@ export const baseUser = (): User => ({
export const useAuthStore = defineStore('user', { export const useAuthStore = defineStore('user', {
state: () => ({ state: () => ({
userData: {} as User, userData: {} as User,
isAuthenticated: false
}), }),
getters: {
isLoggedIn: (state): boolean => state.isAuthenticated,
currentUser: (state): User | null => state.userData,
},
actions: { actions: {
loadUser(userData: User) { loadUser(userData: User) {
this.userData = {...userData}; this.userData = {...userData};
@ -37,7 +43,54 @@ export const useAuthStore = defineStore('user', {
resetUser() { resetUser() {
this.userData = {...baseUser()}; this.userData = {...baseUser()};
}, },
async fetchUser() {
try {
const { data } = await axios.get('/user/profile');
this.userData = snakeToCamel(data);
this.isAuthenticated = true;
} catch (error) {
this.userData = null;
this.isAuthenticated = false;
console.error("Not authenticated or failed to fetch user.", error);
}
},
async login(username, password){
await axios.get('/sanctum/csrf-cookie');
await axios.post('/login', {
username: username,
password: password,
}).then(async (response) => {
let timezone = await axios.get("timezone").then((res) => {
return res.data;
}).catch(error => {
return null;
})
if(response.data.timezone) {
timezone = response.data.timezone
} else {
response.data.timezone = timezone
}
const userData = snakeToCamel(response.data)
this.loadUser(userData);
Settings.defaultZone = timezone;
}).catch((error) => {
return {
"errors": error.response.data.errors,
"loading": false,
}
});
},
async logout(){
try {
await axios.get('/logout');
} catch (error) {
console.error("Error during logout, but clearing state anyway.", error)
} finally {
this.resetUser = null;
this.isAuthenticated = false;
}
},
/** /**
* Updates the current user on the server. * Updates the current user on the server.
* @returns {Promise<any>} A promise that resolves with the response data from the server. * @returns {Promise<any>} A promise that resolves with the response data from the server.
@ -55,8 +108,6 @@ export const useAuthStore = defineStore('user', {
return response.data; return response.data;
}).catch((error: Error) => { }).catch((error: Error) => {
console.error("Error: " + error.message); console.error("Error: " + error.message);
}); });
}, },
} }

View file

@ -15,6 +15,7 @@ use App\Http\Controllers\MusicBrainzController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use App\Http\Controllers\WebstreamController; use App\Http\Controllers\WebstreamController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Spatie\Permission\Models\Role;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -30,6 +31,20 @@ use Illuminate\Support\Facades\Route;
Route::middleware('auth')->get('/user/profile', [UserController::class, 'userProfile']); Route::middleware('auth')->get('/user/profile', [UserController::class, 'userProfile']);
/**
* Misc
*/
Route::middleware('auth')->get('/timezoneList',function() {
return response()->json(DateTimeZone::listIdentifiers(DateTimeZone::ALL));
});
Route::middleware('auth')->get('/roleList',function() {
if(!auth()->user()->hasRole('admin')){
return response()->json(['message' => 'Unauthorized.'], 403);
}
$roles = Role::all()->pluck('name');
return response()->json($roles);
});
/** /**
* Create routes without create method * Create routes without create method
*/ */
@ -79,10 +94,12 @@ Route::get('/rss_podcast_episodes', [PodcastController::class, 'getEpisodes']);
Route::post('/musicbrainz/get_track_metadata', [MusicBrainzController::class, 'get_track_metadata'])->name('musicbrainz.get_track'); Route::post('/musicbrainz/get_track_metadata', [MusicBrainzController::class, 'get_track_metadata'])->name('musicbrainz.get_track');
Route::get('musicbrainz', [MusicBrainzController::class, 'test']); Route::get('musicbrainz', [MusicBrainzController::class, 'test']);
Route::get('testCelery', [PodcastEpisodeController::class, 'testCelery']);
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';
/** /**
* Front-end routes * Front-end routes
*/ */
Route::view('/{vue_capture?}', 'layouts.app')->where('vue_capture', '[\/\w\.-]*'); Route::view('/{vue_capture?}', 'layouts.app')->where('vue_capture', '[\/\w\.-]*');