3 Commits

Author SHA1 Message Date
Adrian Zürcher
df580d98c0 new release
All checks were successful
Build Quasar SPA and Go Backend for memberApp / build-spa (push) Successful in 2m19s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, .exe, windows) (push) Successful in 5m32s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, , linux) (push) Successful in 5m34s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm, 6, , linux) (push) Successful in 5m31s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm64, , linux) (push) Successful in 5m33s
2025-11-08 12:14:05 +01:00
Adrian Zürcher
06afdf4349 add feature to set user defined expiration on every user close #4 2025-11-08 12:09:56 +01:00
Adrian Zürcher
db96732a62 fix functions existing and delete 2025-11-07 08:34:53 +01:00
17 changed files with 121 additions and 40 deletions

View File

@@ -3,8 +3,8 @@ module backend
go 1.24.5 go 1.24.5
require ( require (
gitea.tecamino.com/paadi/access-handler v1.0.13 gitea.tecamino.com/paadi/access-handler v1.0.19
gitea.tecamino.com/paadi/memberDB v1.0.18 gitea.tecamino.com/paadi/memberDB v1.0.21
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 gitea.tecamino.com/paadi/tecamino-dbm v0.1.1
gitea.tecamino.com/paadi/tecamino-logger v0.2.1 gitea.tecamino.com/paadi/tecamino-logger v0.2.1
github.com/gin-contrib/cors v1.7.6 github.com/gin-contrib/cors v1.7.6
@@ -13,7 +13,7 @@ require (
) )
require ( require (
gitea.tecamino.com/paadi/dbHandler v1.0.4 // indirect gitea.tecamino.com/paadi/dbHandler v1.0.8 // indirect
github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect

View File

@@ -1,9 +1,9 @@
gitea.tecamino.com/paadi/access-handler v1.0.13 h1:2BqLo+cmF7ijk8XMnTdNuUKGbWPRsW6jMbk44FMCpyY= gitea.tecamino.com/paadi/access-handler v1.0.19 h1:L51Qg5RNjdIGeQsHwGUTV+ADRpUqPt3qdnu7A7UQbsw=
gitea.tecamino.com/paadi/access-handler v1.0.13/go.mod h1:w71lpnuu5MgAWG3oiI9vsY2dWi4njF/iPrM/xV/dbBQ= gitea.tecamino.com/paadi/access-handler v1.0.19/go.mod h1:wKsB5/Rvaj580gdg3+GbUf5V/0N00XN6cID+C/8135M=
gitea.tecamino.com/paadi/dbHandler v1.0.4 h1:ctnaec0GDdtw3gRQdUISVDYLJ9x+vt50VW41OemfhD4= gitea.tecamino.com/paadi/dbHandler v1.0.8 h1:ZWSBM/KFtLwTv2cBqwK1mOxWAxAfL0BcWEC3kJ9JALU=
gitea.tecamino.com/paadi/dbHandler v1.0.4/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw= gitea.tecamino.com/paadi/dbHandler v1.0.8/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw=
gitea.tecamino.com/paadi/memberDB v1.0.18 h1:S774DtR5t6jpkRfa6uHlrnvTjabHG2KSjfbMmjGkdTM= gitea.tecamino.com/paadi/memberDB v1.0.21 h1:kGQe5fUOc50oQj8caWcjnmmxJaSPuQEjeSG5qDT9Iz4=
gitea.tecamino.com/paadi/memberDB v1.0.18/go.mod h1:iLm7nunVRzqJK8CV4PJVuWIhgPlQjNIaeOkmtfK5fMg= gitea.tecamino.com/paadi/memberDB v1.0.21/go.mod h1:/Af2OeJIHq+8kE5L5DlJxhSJjB75eWBcKRpkxi+n9bU=
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 h1:vAq7mwUxlxJuLzCQSDMrZCwo8ky5usWi9Qz+UP+WnkI= gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 h1:vAq7mwUxlxJuLzCQSDMrZCwo8ky5usWi9Qz+UP+WnkI=
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1/go.mod h1:+tmf1rjPaKEoNeUcr1vdtoFIFweNG3aUGevDAl3NMBk= gitea.tecamino.com/paadi/tecamino-dbm v0.1.1/go.mod h1:+tmf1rjPaKEoNeUcr1vdtoFIFweNG3aUGevDAl3NMBk=
gitea.tecamino.com/paadi/tecamino-logger v0.2.1 h1:sQTBKYPdzn9mmWX2JXZBtGBvNQH7cuXIwsl4TD0aMgE= gitea.tecamino.com/paadi/tecamino-logger v0.2.1 h1:sQTBKYPdzn9mmWX2JXZBtGBvNQH7cuXIwsl4TD0aMgE=

View File

@@ -1,6 +1,6 @@
{ {
"name": "lightcontrol", "name": "lightcontrol",
"version": "1.0.5", "version": "1.0.7",
"description": "A Tecamino App", "description": "A Tecamino App",
"productName": "Member Database", "productName": "Member Database",
"author": "A. Zuercher", "author": "A. Zuercher",

View File

@@ -111,3 +111,5 @@ responsibleAdded: Veratwortläche hinzuegfüegt
responsiblesAdded: Veratwortläche hinzuegfüegt responsiblesAdded: Veratwortläche hinzuegfüegt
deleteResponsible: Veratwortläche entfernt deleteResponsible: Veratwortläche entfernt
deleteResponsibles: Veratwortläche entfernt deleteResponsibles: Veratwortläche entfernt
expiration: Ablauf
never: Nie

View File

@@ -111,3 +111,5 @@ responsibleAdded: Veratwortläche hinzuegfüegt
responsiblesAdded: Verantwortliche hinzuegfüegt responsiblesAdded: Verantwortliche hinzuegfüegt
deleteResponsible: Verantwortliche entfernt deleteResponsible: Verantwortliche entfernt
deleteResponsibles: Verantwortliche entfernt deleteResponsibles: Verantwortliche entfernt
expiration: Ablauf
never: Nie

View File

@@ -111,3 +111,5 @@ responsibleAdded: Responsible hinzuegfüegt
responsiblesAdded: Responsibles hinzuegfüegt responsiblesAdded: Responsibles hinzuegfüegt
deleteResponsible: Responsible deleted deleteResponsible: Responsible deleted
deleteResponsibles: Responsibles deleted deleteResponsibles: Responsibles deleted
expiration: Expiration
never: Never

View File

@@ -55,6 +55,21 @@ appApi.interceptors.response.use(
// Handle unauthorized responses // Handle unauthorized responses
if (error.response?.status === 401 && !originalRequest._retry) { if (error.response?.status === 401 && !originalRequest._retry) {
const data = error.response?.data;
const serverMessage =
typeof data === 'object' && data !== null && 'message' in data
? (data as { message: string }).message
: undefined;
if (['no refresh token', 'is expired'].some((msg) => serverMessage?.includes(msg))) {
console.warn('[Axios] No refresh token — logging out');
try {
await logout();
} catch (logoutErr) {
console.error('[Axios] Logout failed:', logoutErr);
}
throw new Error('Session expired: no refresh token');
}
if (isRefreshing) { if (isRefreshing) {
// Wait until refresh completes // Wait until refresh completes
return new Promise<AxiosResponse>((resolve, reject) => { return new Promise<AxiosResponse>((resolve, reject) => {

View File

@@ -6,7 +6,7 @@ export default boot(async ({ router }) => {
// Restore logic after router is ready but before navigation // Restore logic after router is ready but before navigation
router.isReady().then(() => { router.isReady().then(() => {
const lastRoute = sessionStorage.getItem('lastRoute'); const lastRoute = localStorage.getItem('lastRoute');
const currentPath = router.currentRoute.value.fullPath; const currentPath = router.currentRoute.value.fullPath;
// Restore only if: // Restore only if:
@@ -26,7 +26,7 @@ export default boot(async ({ router }) => {
router.afterEach((to) => { router.afterEach((to) => {
// Don't save login page as "last route" // Don't save login page as "last route"
if (to.path !== '/login' && to.path !== '/') { if (to.path !== '/login' && to.path !== '/') {
sessionStorage.setItem('lastRoute', to.fullPath); localStorage.setItem('lastRoute', to.fullPath);
} }
}); });
}); });

View File

@@ -18,7 +18,15 @@
<q-btn class="q-ma-md" color="primary" no-caps @click="addAttendees">{{ localTitle }}</q-btn> <q-btn class="q-ma-md" color="primary" no-caps @click="addAttendees">{{ localTitle }}</q-btn>
</div> </div>
</DialogFrame> </DialogFrame>
<EditAllDialog ref="newEventRef" v-on:update="(val) => resolveNewEvent(val)"></EditAllDialog> <EditAllDialog
ref="newEventRef"
v-on:update="
(val) => {
resolveNewEvent(val);
NotifyResponse($t('memberUpdated'));
}
"
></EditAllDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -2,24 +2,46 @@
<DialogFrame ref="dialog" :header-title="'Edit ' + localTitle"> <DialogFrame ref="dialog" :header-title="'Edit ' + localTitle">
<div class="row justify-center"> <div class="row justify-center">
<q-input autofocus :label="localTitle" filled v-model="value" @keyup.enter="save"> <q-input autofocus :label="localTitle" filled v-model="value" @keyup.enter="save">
<template v-if="['firstVisit', 'lastVisit', 'date'].includes(localField)" v-slot:prepend> <template
v-if="['firstVisit', 'lastVisit', 'date', 'expiration'].includes(localField)"
v-slot:prepend
>
<q-icon name="event" class="cursor-pointer"> <q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="value" mask="YYYY-MM-DD HH:mm:ss"> <q-date v-model="value" mask="YYYY-MM-DD HH:mm:ss">
<div class="row items-center justify-end"> <div class="row items-center justify-end">
<q-btn :label="$t('now')" color="primary" no-caps flat @click="setTimeNow" /> <q-btn :label="$t('now')" color="primary" no-caps flat @click="setTimeNow" />
<q-btn
v-if="localField"
:label="$t('never')"
color="primary"
no-caps
flat
@click="value = 'never'"
/>
<q-btn no-caps v-close-popup :label="$t('close')" color="primary" flat /> <q-btn no-caps v-close-popup :label="$t('close')" color="primary" flat />
</div> </div>
</q-date> </q-date>
</q-popup-proxy> </q-popup-proxy>
</q-icon> </q-icon>
</template> </template>
<template v-if="['firstVisit', 'lastVisit', 'date'].includes(localField)" v-slot:append> <template
v-if="['firstVisit', 'lastVisit', 'date', 'expiration'].includes(localField)"
v-slot:append
>
<q-icon name="access_time" class="cursor-pointer"> <q-icon name="access_time" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-time with-seconds v-model="value" mask="YYYY-MM-DD HH:mm:ss" format24h> <q-time with-seconds v-model="value" mask="YYYY-MM-DD HH:mm:ss" format24h>
<div class="row items-center justify-end"> <div class="row items-center justify-end">
<q-btn :label="$t('now')" color="primary" no-caps flat @click="setTimeNow" /> <q-btn :label="$t('now')" color="primary" no-caps flat @click="setTimeNow" />
<q-btn
v-if="localField"
:label="$t('never')"
color="primary"
no-caps
flat
@click="value = 'never'"
/>
<q-btn no-caps v-close-popup :label="$t('close')" color="primary" flat /> <q-btn no-caps v-close-popup :label="$t('close')" color="primary" flat />
</div> </div>
</q-time> </q-time>
@@ -40,7 +62,6 @@ import { ref } from 'vue';
import { appApi } from 'src/boot/axios'; import { appApi } from 'src/boot/axios';
import type { Member } from 'src/vueLib/models/member'; import type { Member } from 'src/vueLib/models/member';
import { useNotify } from 'src/vueLib/general/useNotify'; import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'src/boot/lang';
const dialog = ref(); const dialog = ref();
const localMember = ref(); const localMember = ref();
@@ -51,7 +72,6 @@ const value = ref('');
const props = defineProps({ const props = defineProps({
endpoint: { endpoint: {
type: String, type: String,
required: true,
}, },
}); });
@@ -74,6 +94,12 @@ function save() {
dialog.value.close(); dialog.value.close();
return; return;
} }
if (!props.endpoint) {
localMember.value[localField.value] = value.value;
emit('update', localMember.value);
return;
}
payload = [ payload = [
{ {
id: localMember.value.id, id: localMember.value.id,
@@ -85,7 +111,6 @@ function save() {
.post(props.endpoint, payload) .post(props.endpoint, payload)
.then(() => { .then(() => {
emit('update'); emit('update');
NotifyResponse(i18n.global.t('memberUpdated'));
dialog.value.close(); dialog.value.close();
}) })
.catch((err) => { .catch((err) => {

View File

@@ -91,10 +91,11 @@
v-model="localUser.role" v-model="localUser.role"
></q-select> ></q-select>
<q-input <q-input
class="col-5" class="col-5 q-mt-xl"
:label="$t('expires')" :label="$t('expires')"
filled filled
v-model="localUser.expires" type="datetime-local"
v-model="localUser.expiration"
></q-input> ></q-input>
</div> </div>
</div> </div>
@@ -131,7 +132,6 @@ const localUser = ref<User>({
user: '', user: '',
email: '', email: '',
role: '', role: '',
expires: '',
}); });
const props = defineProps({ const props = defineProps({
@@ -156,7 +156,6 @@ async function open(user: User | null) {
user: '', user: '',
email: '', email: '',
role: '', role: '',
expires: '',
}; };
newUser.value = true; newUser.value = true;
} }

View File

@@ -21,7 +21,7 @@ onMounted(() => {
const forwardToPage = async () => { const forwardToPage = async () => {
await nextTick(); await nextTick();
const lastRoute = sessionStorage.getItem('lastRoute') || '/members'; const lastRoute = localStorage.getItem('lastRoute') || '/members';
await router.push(lastRoute); await router.push(lastRoute);
}; };
</script> </script>

View File

@@ -48,7 +48,7 @@ export function useLogin() {
}); });
userStore.clearUser(); userStore.clearUser();
sessionStorage.clear(); localStorage.clear();
stopRefreshInterval(); stopRefreshInterval();
} }

View File

@@ -5,7 +5,7 @@ export interface User {
user: string; user: string;
email: string; email: string;
role: string; role: string;
expires: string; expiration?: string;
password?: string; password?: string;
settings?: Settings; settings?: Settings;
} }

View File

@@ -116,6 +116,7 @@
<q-td :props="props"> <q-td :props="props">
<q-select <q-select
v-if="responsibles.length > 0" v-if="responsibles.length > 0"
:readonly="!user.isPermittedTo('members', 'write')"
:options="responsibles" :options="responsibles"
:option-label="(opt) => opt.firstName + ' ' + opt.lastName" :option-label="(opt) => opt.firstName + ' ' + opt.lastName"
v-model="props.row.responsiblePerson" v-model="props.row.responsiblePerson"
@@ -163,12 +164,12 @@
ref="editOneDialog" ref="editOneDialog"
endpoint="members/edit" endpoint="members/edit"
query-id query-id
v-on:update="updateMembers" v-on:update="updateMember"
></EditOneDialog> ></EditOneDialog>
<EditAllDialog <EditAllDialog
ref="editAllDialog" ref="editAllDialog"
:responsibles="responsibles" :responsibles="responsibles"
v-on:update="updateMembers" v-on:update="updateMember"
></EditAllDialog> ></EditAllDialog>
<OkDialog <OkDialog
ref="okDialog" ref="okDialog"
@@ -325,11 +326,13 @@ function removeMember(...removeMembers: Members) {
}); });
} }
function updateMember(member: Member) { function updateMember(member: Member | null) {
if (!member) NotifyResponse(i18n.global.t('memberUpdated'));
appApi appApi
.post('/members/edit', [member]) .post('/members/edit', [member])
.then(() => NotifyResponse(i18n.global.t('memberUpdated'))) .then(() => NotifyResponse(i18n.global.t('memberUpdated')))
.catch((err) => NotifyResponse(err, 'error')); .catch((err) => NotifyResponse(err, 'error'));
updateMembers().catch((err) => NotifyResponse(err, 'error'));
} }
function addToEvent() { function addToEvent() {

View File

@@ -37,6 +37,14 @@ export function useUserTable() {
sortable: true, sortable: true,
style: 'width: 120px; max-width: 120px;', style: 'width: 120px; max-width: 120px;',
}, },
{
name: 'expiration',
align: 'left' as const,
label: i18n.global.t('expiration'),
field: 'expiration',
sortable: true,
style: 'width: 120px; max-width: 120px;',
},
{ name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' }, { name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' },
]); ]);

View File

@@ -67,17 +67,7 @@
</q-input> </q-input>
</template> </template>
<template v-slot:body-cell="props"> <template v-slot:body-cell="props">
<q-td v-if="props.col.name === 'role'" :props="props">
<q-select
:readonly="!user.isPermittedTo('userSettings', 'write') || !autorized(props.row)"
dense
v-model="props.row.role"
:options="localRoles"
@update:model-value="updateUser(props.row)"
></q-select>
</q-td>
<q-td <q-td
v-else
:props="props" :props="props"
:style=" :style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write') autorized(props.row) && user.isPermittedTo('userSettings', 'write')
@@ -93,6 +83,34 @@
{{ props.value }} {{ props.value }}
</q-td> </q-td>
</template> </template>
<template v-slot:body-cell-role="props">
<q-td :props="props">
<q-select
:readonly="!user.isPermittedTo('userSettings', 'write') || !autorized(props.row)"
dense
v-model="props.row.role"
:options="localRoles"
@update:model-value="updateUser(props.row)"
></q-select>
</q-td>
</template>
<template v-slot:body-cell-expiration="props">
<q-td
:props="props"
:style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? 'cursor: pointer'
: ''
"
@click="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
>
{{ props.value === 'never' ? $t('never') : props.value }}
</q-td>
</template>
<template v-slot:body-cell-option="props"> <template v-slot:body-cell-option="props">
<q-td :props="props"> <q-td :props="props">
<q-btn <q-btn
@@ -112,9 +130,8 @@
</div> </div>
<EditOneDialog <EditOneDialog
ref="editOneDialog" ref="editOneDialog"
endpoint="users/edit"
query-id query-id
v-on:update="updateUsers" v-on:update="(val) => updateUser(val)"
></EditOneDialog> ></EditOneDialog>
<EditAllDialog ref="editAllDialog" :roles="localRoles" v-on:update="updateUsers"></EditAllDialog> <EditAllDialog ref="editAllDialog" :roles="localRoles" v-on:update="updateUsers"></EditAllDialog>
<OkDialog <OkDialog