diff --git a/backend/go.mod b/backend/go.mod index addd446..e2254f4 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,8 +3,8 @@ module backend go 1.24.5 require ( - gitea.tecamino.com/paadi/access-handler v1.0.25 - gitea.tecamino.com/paadi/memberDB v1.1.3 + gitea.tecamino.com/paadi/access-handler v1.0.29 + gitea.tecamino.com/paadi/memberDB v1.1.13 gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 gitea.tecamino.com/paadi/tecamino-logger v0.2.1 github.com/gin-contrib/cors v1.7.6 @@ -14,7 +14,7 @@ require ( ) require ( - gitea.tecamino.com/paadi/dbHandler v1.0.8 // indirect + gitea.tecamino.com/paadi/dbHandler v1.1.7 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect diff --git a/backend/go.sum b/backend/go.sum index dce7756..c7e4934 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,9 +1,9 @@ -gitea.tecamino.com/paadi/access-handler v1.0.25 h1:GiMnkEM0/fo2B1uCzGVyjpAhM2S58LG22N6+BdtdpgQ= -gitea.tecamino.com/paadi/access-handler v1.0.25/go.mod h1:wKsB5/Rvaj580gdg3+GbUf5V/0N00XN6cID+C/8135M= -gitea.tecamino.com/paadi/dbHandler v1.0.8 h1:ZWSBM/KFtLwTv2cBqwK1mOxWAxAfL0BcWEC3kJ9JALU= -gitea.tecamino.com/paadi/dbHandler v1.0.8/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw= -gitea.tecamino.com/paadi/memberDB v1.1.3 h1:ZwSA+TNL1ZvL8bMnJ5a2odc44bQBa31gVxD2fBA6o0I= -gitea.tecamino.com/paadi/memberDB v1.1.3/go.mod h1:/Af2OeJIHq+8kE5L5DlJxhSJjB75eWBcKRpkxi+n9bU= +gitea.tecamino.com/paadi/access-handler v1.0.29 h1:FZ67co/rfJffftT6xOp6psZKFtdEReaAR7PnEZi7ltI= +gitea.tecamino.com/paadi/access-handler v1.0.29/go.mod h1:Dmme8URu3lENPhlkZcdEeIKm8VMlAgT/jNLECLLS7Vs= +gitea.tecamino.com/paadi/dbHandler v1.1.7 h1:NqVbxbUwd7EZX6HYntyLYwwPbyTPevOhIBTFqoCVqOU= +gitea.tecamino.com/paadi/dbHandler v1.1.7/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw= +gitea.tecamino.com/paadi/memberDB v1.1.13 h1:P5UsTt3d8829H9d3vfMAWpDN7ONqwhr8ndIuL9lBuvQ= +gitea.tecamino.com/paadi/memberDB v1.1.13/go.mod h1:FRbhFgXq4jDpfCrCfHCVr7VcA44fR8J3XXQFeO6QSBk= 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-logger v0.2.1 h1:sQTBKYPdzn9mmWX2JXZBtGBvNQH7cuXIwsl4TD0aMgE= diff --git a/src/boot/auth.ts b/src/boot/auth.ts index 83db2aa..cb9b50d 100644 --- a/src/boot/auth.ts +++ b/src/boot/auth.ts @@ -15,7 +15,11 @@ export default boot(async ({ app }) => { .get('/login/me') .then((resp) => { useStore - .setUser({ id: resp.data.id, username: resp.data.username, role: resp.data.role }) + .setUser({ + id: resp.data.id, + username: resp.data.username, + role: { role: resp.data.role, permissions: [] }, + }) .catch((err) => console.error(err)); login.refresh().catch((err) => console.error(err)); }) diff --git a/src/boot/restore-route.js b/src/boot/restore-route.js index 2405cb5..2dfe54f 100644 --- a/src/boot/restore-route.js +++ b/src/boot/restore-route.js @@ -8,6 +8,8 @@ export default boot(async ({ router }) => { // load user try { const { data } = await appApi.get('/login/me'); + + data.role.role = data.role; await userStore.setUser(data); } catch { /* ignore error */ diff --git a/src/components/MemberEditAllDialog.vue b/src/components/MemberEditAllDialog.vue index b527372..b043ce1 100644 --- a/src/components/MemberEditAllDialog.vue +++ b/src/components/MemberEditAllDialog.vue @@ -77,7 +77,7 @@ filled :options="props.responsibles" :option-label="(opt) => opt.firstName + ' ' + opt.lastName" - v-model="localMember.responsiblePerson" + v-model="localMember.responsible" > ({ const props = defineProps({ responsibles: { - type: Object as PropType, + type: Object as PropType, }, group: { type: Array, diff --git a/src/components/UserEditAllDialog.vue b/src/components/UserEditAllDialog.vue index ef4d51b..3b21a77 100644 --- a/src/components/UserEditAllDialog.vue +++ b/src/components/UserEditAllDialog.vue @@ -24,63 +24,7 @@ :rules="[(val) => !!val || $t('emailIsRequired')]" v-model="localUser.email" > -
- - - - -
- -
- {{ strengthLabel }} -
-
- - - -
- +
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue'; import { ref } from 'vue'; -import zxcvbn from 'zxcvbn'; import { appApi } from 'src/boot/axios'; import type { User } from 'src/vueLib/models/users'; import { useNotify } from 'src/vueLib/general/useNotify'; import { validateQForm } from 'src/vueLib/utils/validation'; import { i18n } from 'src/boot/lang'; import { DefaultSettings } from 'src/vueLib/models/settings'; +import EnterNewPassword from 'src/vueLib/login/EnterNewPassword.vue'; const { NotifyResponse } = useNotify(); const dialog = ref(); const form = ref(); const newUser = ref(false); -const showPassword1 = ref(false); -const showPassword2 = ref(false); -const passwordCheck = ref(''); -const strengthValue = ref(0); -const strengthLabel = ref('Enter a password'); -const strengthColor = ref('grey'); -const strengthIcon = ref('lock'); +const role = ref(''); const localUser = ref({ user: '', email: '', - role: '', }); const props = defineProps({ @@ -155,7 +92,6 @@ async function open(user: User | null) { localUser.value = { user: '', email: '', - role: '', }; newUser.value = true; } @@ -164,47 +100,6 @@ async function open(user: User | null) { await validateQForm(form.value); } -function checkStrength() { - const result = zxcvbn(localUser.value.password || ''); - strengthValue.value = (result.score + 1) / 5; - const levels = [ - i18n.global.t('veryWeak'), - i18n.global.t('weak'), - i18n.global.t('fair'), - i18n.global.t('good'), - i18n.global.t('strong'), - ]; - const colors = ['red', 'orange', 'yellow', 'light-green', 'green']; - const icon = ['lock', 'warning', 'error_outline', 'check_circle_outline', 'verified_user']; - - strengthLabel.value = levels[result.score] || i18n.global.t('veryWeak'); - strengthColor.value = colors[result.score] || 'grey'; - strengthIcon.value = icon[result.score] || 'lock'; -} - -function validatePassword(): string | boolean { - if (!localUser.value.password) return i18n.global.t('passwordIsRequired'); - - if (localUser.value.password.length < 8) { - return i18n.global.t('passwordTooShort'); - } else if (!/[A-Z]/.test(localUser.value.password)) { - return i18n.global.t('passwordNeedsUppercase'); - } else if (!/[a-z]/.test(localUser.value.password)) { - return i18n.global.t('passwordNeedsLowercase'); - } else if (!/[0-9]/.test(localUser.value.password)) { - return i18n.global.t('passwordNeedsNumber'); - } else if (!/[!@#$%^&*(),.?":{}|<>]/.test(localUser.value.password)) { - return i18n.global.t('passwordNeedsSpecial'); - } - - return true; -} - -function checkSamePassword(): string | boolean { - if (localUser.value.password === passwordCheck.value) return true; - return i18n.global.t('passwordDoNotMatch'); -} - async function save() { if (!(await validateQForm(form.value))) { NotifyResponse(i18n.global.t('notAllRequiredFieldsFilled'), 'error'); @@ -216,6 +111,7 @@ async function save() { localUser.value.settings = DefaultSettings(); } + localUser.value.role = { role: role.value || '', permissions: [] }; appApi .post(query, JSON.stringify(localUser.value)) .then(() => { diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue index 6d26fce..41ccab8 100644 --- a/src/pages/LoginPage.vue +++ b/src/pages/LoginPage.vue @@ -15,7 +15,7 @@ const router = useRouter(); const userStore = useUserStore(); onMounted(() => { - if (userStore.user?.username !== '' && userStore.user?.role !== '') { + if (userStore.user?.username !== '' && userStore.user?.role.role !== '') { forwardToPage().catch((err) => console.error(err)); } }); diff --git a/src/pages/SettingsPage.vue b/src/pages/SettingsPage.vue index fecc9e3..3143024 100644 --- a/src/pages/SettingsPage.vue +++ b/src/pages/SettingsPage.vue @@ -194,6 +194,7 @@ function save() { setLocalSettings(settings); const tempuser = user.user; + if (tempuser) { tempuser.settings = settings; } diff --git a/src/vueLib/general/SearchableSelect .vue b/src/vueLib/general/SearchableSelect .vue index a41503c..3fd7649 100644 --- a/src/vueLib/general/SearchableSelect .vue +++ b/src/vueLib/general/SearchableSelect .vue @@ -44,7 +44,7 @@ const props = defineProps({ required: true, }, optionLabel: { - type: [Function, String] as PropType<((option: T) => string) | string>, + type: [Function, String] as PropType<((option: T) => string) | string | undefined>, required: true, }, optionValue: { @@ -61,7 +61,7 @@ const optionLabel = props.optionLabel; const optionValue = props.optionValue; const modelValueLocal = ref(props.modelValue); -const filteredOptions = ref([...props.options]); +const filteredOptions = ref([...(props.options || [])]); function searchBox() { if (search.value) { @@ -91,7 +91,7 @@ function filterFn(val: string, update: (fn: () => void) => void) { if (typeof optionLabel === 'function') { field = optionLabel(opt); } else { - field = opt[optionLabel]; + field = opt[String(optionLabel)]; } if (typeof field !== 'string' && typeof field !== 'number') return false; diff --git a/src/vueLib/login/userStore.ts b/src/vueLib/login/userStore.ts index d96196a..67a80e7 100644 --- a/src/vueLib/login/userStore.ts +++ b/src/vueLib/login/userStore.ts @@ -20,7 +20,7 @@ export const useUserStore = defineStore('user', { isAuthorizedAs: (state: UserState) => { return (roles: string[]) => { - return state.user !== null && roles.includes(state.user.role); + return state.user !== null && roles.includes(state.user.role?.role); }; }, isPermittedTo: (state: UserState) => { @@ -44,9 +44,9 @@ export const useUserStore = defineStore('user', { actions: { async setUser(user: User) { await appApi - .get('roles?role=' + user.role) + .get('roles?role=' + user.role?.role) .then((resp) => { - const roleData = resp.data.find((role: Role) => role.role === user.role); + const roleData = resp.data.find((role: Role) => role.role === user.role?.role); user.permissions = roleData?.permissions || []; this.user = user; }) diff --git a/src/vueLib/models/member.ts b/src/vueLib/models/member.ts index 943f2ef..4083b69 100644 --- a/src/vueLib/models/member.ts +++ b/src/vueLib/models/member.ts @@ -1,5 +1,10 @@ +import type { Group } from './group'; +import type { Responsible } from './responsible'; + export interface Member { id: number; + memberId?: number; + responsibleId?: number | undefined; firstName: string; lastName: string; birthday?: string; @@ -10,8 +15,8 @@ export interface Member { zip?: string; phone?: string; email?: string; - group?: string; - responsiblePerson?: Member; + group?: Group; + responsible?: Responsible; firstVisit?: string; lastVisit?: string; } diff --git a/src/vueLib/models/responsible.ts b/src/vueLib/models/responsible.ts new file mode 100644 index 0000000..a8ea887 --- /dev/null +++ b/src/vueLib/models/responsible.ts @@ -0,0 +1,9 @@ +import type { Member } from './member'; + +export interface Responsible { + id: number; + memberId: number; + member: Member; +} + +export type Responsibles = Responsible[]; diff --git a/src/vueLib/models/user.ts b/src/vueLib/models/user.ts index 5028109..25dce0d 100644 --- a/src/vueLib/models/user.ts +++ b/src/vueLib/models/user.ts @@ -1,10 +1,11 @@ import type { Permissions } from '../checkboxes/permissions'; +import type { Role } from './roles'; import type { Settings } from './settings'; export interface User { id: number; username: string; - role: string; + role: Role; permissions?: Permissions; settings?: Settings; } diff --git a/src/vueLib/tables/attendees/AttendeesTable.vue b/src/vueLib/tables/attendees/AttendeesTable.vue index cd6046b..697c7a0 100644 --- a/src/vueLib/tables/attendees/AttendeesTable.vue +++ b/src/vueLib/tables/attendees/AttendeesTable.vue @@ -73,7 +73,7 @@ {{ - props.row.count + props.row.attendees.length }} diff --git a/src/vueLib/tables/components/ClickableComponent.vue b/src/vueLib/tables/components/ClickableComponent.vue new file mode 100644 index 0000000..f1a114a --- /dev/null +++ b/src/vueLib/tables/components/ClickableComponent.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/vueLib/tables/components/FilterSelect.vue b/src/vueLib/tables/components/FilterSelect.vue new file mode 100644 index 0000000..f3509cd --- /dev/null +++ b/src/vueLib/tables/components/FilterSelect.vue @@ -0,0 +1,49 @@ + + diff --git a/src/vueLib/tables/components/IconComponent.vue b/src/vueLib/tables/components/IconComponent.vue new file mode 100644 index 0000000..9512edd --- /dev/null +++ b/src/vueLib/tables/components/IconComponent.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/vueLib/tables/components/MenuComponent.vue b/src/vueLib/tables/components/MenuComponent.vue new file mode 100644 index 0000000..8b1867c --- /dev/null +++ b/src/vueLib/tables/components/MenuComponent.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/vueLib/tables/components/SearchableInput.vue b/src/vueLib/tables/components/SearchableInput.vue new file mode 100644 index 0000000..81e3549 --- /dev/null +++ b/src/vueLib/tables/components/SearchableInput.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/vueLib/tables/components/SearchableSelect.vue b/src/vueLib/tables/components/SearchableSelect.vue new file mode 100644 index 0000000..a9286cc --- /dev/null +++ b/src/vueLib/tables/components/SearchableSelect.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/vueLib/tables/components/TopButtonGroup.vue b/src/vueLib/tables/components/TopButtonGroup.vue new file mode 100644 index 0000000..8116889 --- /dev/null +++ b/src/vueLib/tables/components/TopButtonGroup.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/vueLib/tables/events/EventsTable.vue b/src/vueLib/tables/events/EventsTable.vue index 7e746a2..23d9f48 100644 --- a/src/vueLib/tables/events/EventsTable.vue +++ b/src/vueLib/tables/events/EventsTable.vue @@ -23,27 +23,14 @@ class="bigger-table-text" > + + + + + + +
@@ -159,6 +128,8 @@ import { useUserStore } from 'src/vueLib/login/userStore'; import AttendeesTableDialog from '../attendees/AttendeesTableDialog.vue'; import type { Members } from 'src/vueLib/models/member'; import { i18n } from 'src/boot/lang'; +import SearchableInput from '../components/SearchableInput.vue'; +import TopButtonGroup from '../components/TopButtonGroup.vue'; export interface EventDialog { getSelected: () => Events; diff --git a/src/vueLib/tables/group/GroupTable.ts b/src/vueLib/tables/group/GroupTable.ts index 70fb809..2c4e742 100644 --- a/src/vueLib/tables/group/GroupTable.ts +++ b/src/vueLib/tables/group/GroupTable.ts @@ -3,6 +3,8 @@ import { ref, computed } from 'vue'; import type { Members } from 'src/vueLib/models/member'; import { useNotify } from 'src/vueLib/general/useNotify'; import { i18n } from 'boot/lang'; +import ClickableComponent from '../components/ClickableComponent.vue'; +import MenuComponent from '../components/MenuComponent.vue'; export function useGroupTable() { const groups = ref([]); @@ -21,8 +23,16 @@ export function useGroupTable() { label: i18n.global.t('name'), field: 'name', sortable: true, + component: ClickableComponent, + }, + { + name: 'option', + align: 'center' as const, + label: '', + field: 'option', + icon: 'option', + component: MenuComponent, }, - { name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' }, ]); const { NotifyResponse } = useNotify(); diff --git a/src/vueLib/tables/group/GroupTable.vue b/src/vueLib/tables/group/GroupTable.vue index c2a030e..cb623a3 100644 --- a/src/vueLib/tables/group/GroupTable.vue +++ b/src/vueLib/tables/group/GroupTable.vue @@ -23,27 +23,16 @@ class="bigger-table-text" > + + + + + - +
Groups; -} +import SearchableInput from '../components/SearchableInput.vue'; +import TopButtonGroup from '../components/TopButtonGroup.vue'; const { NotifyResponse } = useNotify(); const groupDialog = ref(); diff --git a/src/vueLib/tables/members/MembersTable.ts b/src/vueLib/tables/members/MembersTable.ts index b978414..a42e2c6 100644 --- a/src/vueLib/tables/members/MembersTable.ts +++ b/src/vueLib/tables/members/MembersTable.ts @@ -6,6 +6,10 @@ import { i18n } from 'boot/lang'; import { useResponsibleTable } from '../responsible/ResponsibleTable'; import { appName } from 'src/vueLib/models/settings'; import { useGroupTable } from '../group/GroupTable'; +import IconComponent from '../components/IconComponent.vue'; +import SearchableSelectComponent from '../components/SearchableSelect.vue'; +import ClickableComponent from '../components/ClickableComponent.vue'; +import MenuComponent from '../components/MenuComponent.vue'; export function useMemberTable() { const allMembers = ref([]); @@ -16,7 +20,7 @@ export function useMemberTable() { keys: string[]; }[] >(); - const { responsibles, updateResponsibles } = useResponsibleTable(); + const { responsibles, responsibleMember, updateResponsibles } = useResponsibleTable(); const { groups, updateGroups } = useGroupTable(); const pagination = ref({ @@ -40,21 +44,30 @@ export function useMemberTable() { phone: true, email: true, group: true, - responsiblePerson: true, + responsible: true, firstVisit: true, lastVisit: true, option: true, + test: true, }); const columns = computed(() => [ - { name: 'cake', align: 'center' as const, label: '', field: 'cake', icon: 'cake' }, + { + name: 'cake', + align: 'center' as const, + label: '', + field: 'cake', + icon: 'cake', + component: IconComponent, + }, { name: 'firstName', align: 'left' as const, label: i18n.global.t('prename'), field: 'firstName', sortable: true, + component: ClickableComponent, }, { name: 'lastName', @@ -62,6 +75,7 @@ export function useMemberTable() { label: i18n.global.t('lastName'), field: 'lastName', sortable: true, + component: ClickableComponent, }, { name: 'birthday', @@ -69,6 +83,7 @@ export function useMemberTable() { label: i18n.global.t('birthday'), field: 'birthday', sortable: true, + component: ClickableComponent, }, { name: 'age', @@ -76,6 +91,7 @@ export function useMemberTable() { label: i18n.global.t('age'), field: 'age', sortable: true, + component: ClickableComponent, }, { name: 'comment', @@ -83,6 +99,7 @@ export function useMemberTable() { label: i18n.global.t('comment'), field: 'comment', sortable: true, + component: ClickableComponent, }, { name: 'address', @@ -90,6 +107,7 @@ export function useMemberTable() { label: i18n.global.t('address'), field: 'address', sortable: true, + component: ClickableComponent, }, { name: 'town', @@ -97,6 +115,7 @@ export function useMemberTable() { label: i18n.global.t('town'), field: 'town', sortable: true, + component: ClickableComponent, }, { name: 'zip', @@ -104,6 +123,7 @@ export function useMemberTable() { label: i18n.global.t('zipCode'), field: 'zip', sortable: true, + component: ClickableComponent, }, { name: 'phone', @@ -111,6 +131,7 @@ export function useMemberTable() { label: i18n.global.t('phone'), field: 'phone', sortable: true, + component: ClickableComponent, }, { name: 'email', @@ -118,6 +139,7 @@ export function useMemberTable() { label: i18n.global.t('email'), field: 'email', sortable: true, + component: ClickableComponent, }, { name: 'group', @@ -125,13 +147,15 @@ export function useMemberTable() { label: i18n.global.t('group'), field: 'group', sortable: true, + component: SearchableSelectComponent, }, { - name: 'responsiblePerson', + name: 'responsible', align: 'left' as const, label: i18n.global.t('responsible'), - field: 'responsiblePerson', + field: 'responsible', sortable: true, + component: SearchableSelectComponent, }, { name: 'firstVisit', @@ -139,6 +163,7 @@ export function useMemberTable() { label: i18n.global.t('firstVisit'), field: 'firstVisit', sortable: true, + component: ClickableComponent, }, { name: 'lastVisit', @@ -146,8 +171,16 @@ export function useMemberTable() { label: i18n.global.t('lastVisit'), field: 'lastVisit', sortable: true, + component: ClickableComponent, + }, + { + name: 'option', + align: 'center' as const, + label: '', + field: 'option', + icon: 'option', + component: MenuComponent, }, - { name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' }, ].filter((c) => enabledColumns.value[c.name]), ); @@ -225,11 +258,13 @@ export function useMemberTable() { return; } allMembers.value.forEach((member) => { - if (!responsibles.value.some((r) => r.id === member.responsiblePerson?.id)) { - delete member.responsiblePerson; + // remove responsible of member if it does not exist in responsible list any longer + if (!responsibles.value.some((r) => r.id === member.responsible?.id)) { + delete member.responsible; } - if (member.birthday !== undefined) { + //calculte years if a birth date is entered + if (member.birthday) { member.age = String(calculateAge(member.birthday)); } }); @@ -314,9 +349,14 @@ export function useMemberTable() { exportableColumns .map((col) => { const value = member[col.field]; - // handle nested objects (e.g. responsiblePerson) + // handle nested objects (e.g. responsible) if (typeof value === 'object' && value !== null) { - if ('firstName' in value && 'lastName' in value) + if ( + 'firstName' in value && + 'lastName' in value && + typeof value.firstName === 'string' && + typeof value.lastName === 'string' + ) return `"${value.firstName} ${value.lastName}"`; return `"${JSON.stringify(value)}"`; } @@ -344,6 +384,7 @@ export function useMemberTable() { allMembers, filteredMembers, responsibles, + responsibleMember, groups, pagination, columns, diff --git a/src/vueLib/tables/members/MembersTable.vue b/src/vueLib/tables/members/MembersTable.vue index 9ed5f4a..fb25a49 100644 --- a/src/vueLib/tables/members/MembersTable.vue +++ b/src/vueLib/tables/members/MembersTable.vue @@ -22,48 +22,24 @@ class="bigger-table-text" > - - + + + - + + + + + +