1
0
Fork 0
forked from Kispi/Core

feat(webapp): add banker login
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon Giesel 2023-07-17 20:21:59 +02:00
parent 0b087a38bf
commit 36638fbf74
10 changed files with 291 additions and 118 deletions

View file

@ -1,111 +1,4 @@
[
{
"id": "w85pgrtrmovf916",
"name": "transactions",
"type": "base",
"system": false,
"schema": [
{
"id": "5aeh5giq",
"name": "account",
"type": "relation",
"system": false,
"required": true,
"options": {
"collectionId": "74ftooxenpeq14b",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": [
"firstName",
"lastName"
]
}
},
{
"id": "2bzqyeuk",
"name": "label",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "zsx9i8w7",
"name": "amount",
"type": "number",
"system": false,
"required": false,
"options": {
"min": null,
"max": null
}
}
],
"indexes": [],
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "5msnfxat1sc2c1r",
"name": "companyTransactions",
"type": "base",
"system": false,
"schema": [
{
"id": "7rnyupog",
"name": "account",
"type": "relation",
"system": false,
"required": true,
"options": {
"collectionId": "s854d2w72fvyl54",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": []
}
},
{
"id": "ruby9fp9",
"name": "label",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "4hiu46ib",
"name": "amount",
"type": "number",
"system": false,
"required": false,
"options": {
"min": null,
"max": null
}
}
],
"indexes": [],
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "s854d2w72fvyl54",
"name": "companies",
@ -431,5 +324,135 @@
"updateRule": "(@request.data.id = null && @request.data.accountNumber = null && @request.data.firstName = null && @request.data.lastName = null && @request.data.grade = null && @request.data.created = null && @request.data.updated = null && @request.data.company = null && @request.data.shift = null && @request.data.wage = null) || (@request.data.id = null && @request.data.accountNumber = null && @request.data.firstName = null && @request.data.lastName = null && @request.data.created = null && @request.data.updated = null && @request.data.company = null && @request.auth.id = company.id)",
"deleteRule": null,
"options": {}
},
{
"id": "w1au07idupp27qv",
"name": "bankers",
"type": "auth",
"system": false,
"schema": [],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
},
{
"id": "w85pgrtrmovf916",
"name": "transactions",
"type": "base",
"system": false,
"schema": [
{
"id": "5aeh5giq",
"name": "account",
"type": "relation",
"system": false,
"required": true,
"options": {
"collectionId": "74ftooxenpeq14b",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": [
"firstName",
"lastName"
]
}
},
{
"id": "2bzqyeuk",
"name": "label",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "zsx9i8w7",
"name": "amount",
"type": "number",
"system": false,
"required": false,
"options": {
"min": null,
"max": null
}
}
],
"indexes": [],
"listRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
"viewRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
"createRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "5msnfxat1sc2c1r",
"name": "companyTransactions",
"type": "base",
"system": false,
"schema": [
{
"id": "7rnyupog",
"name": "account",
"type": "relation",
"system": false,
"required": true,
"options": {
"collectionId": "s854d2w72fvyl54",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": []
}
},
{
"id": "ruby9fp9",
"name": "label",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "4hiu46ib",
"name": "amount",
"type": "number",
"system": false,
"required": false,
"options": {
"min": null,
"max": null
}
}
],
"indexes": [],
"listRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
"viewRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
"createRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
"updateRule": null,
"deleteRule": null,
"options": {}
}
]

View file

@ -1,6 +1,7 @@
<template>
<div class="relative">
<input
ref="input"
:value="value"
type="number"
class="input w-full border-primary text-right"
@ -26,6 +27,10 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const input = ref<InstanceType<typeof HTMLInputElement>>();
const props = withDefaults(defineProps<{
suffixMargin?: boolean,
perHour?: boolean,
@ -53,9 +58,19 @@ function onKeyDown(event: KeyboardEvent) {
}
}
function reset() {
if(input.value) {
input.value.value = '';
}
}
const emit = defineEmits<{
change: [value: number],
}>();
defineExpose({
reset,
});
</script>
<style lang="scss" scoped></style>

View file

@ -5,6 +5,9 @@
:required="required"
:placeholder="placeholder"
class="input w-full border-primary"
:class="{
'!border-error': error,
}"
/>
</template>
@ -22,6 +25,10 @@ defineProps({
type: String,
default: '',
},
error: {
type: Boolean,
default: false,
},
});
/* global defineModel */

View file

@ -0,0 +1,63 @@
<template>
<div class="hero min-h-full">
<form
class="hero-content mx-auto flex-col text-center"
@submit.prevent="handleAuth"
>
<AtomInput
v-model="username"
placeholder="Nutzername"
type="text"
:error="error"
required
/>
<AtomInput
v-model="password"
placeholder="Passwort"
type="password"
:error="error"
required
/>
<div
v-if="error"
class="alert alert-error flex w-[211px] items-center gap-2"
>
<XCircleIcon class="h-12 w-12" />
<span class="text-base font-normal">Nutzername oder Passwort falsch.</span>
</div>
<button class="btn-primary btn gap-2 self-end">
<ArrowRightOnRectangleIcon class="h-6 w-6" />
Login
</button>
</form>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { AuthService } from '../../services/auth.service';
import { ArrowRightOnRectangleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
import AtomInput from '../atoms/AtomInput.vue';
const username = ref('');
const password = ref('');
const error = ref(false);
async function handleAuth() {
try {
await AuthService.loginAsBanker(username.value, password.value);
} catch (e) {
error.value = true;
console.error(e);
}
}
watch([username, password], () => {
error.value = false;
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -8,8 +8,9 @@
<div class="flex place-items-center justify-between">
<span>{{ inputLabel }}</span>
<AtomCurrencyInput
v-model="amount"
ref="input"
class="w-4/12"
@change="amount = $event"
@keyup.esc="modal?.close()"
/>
</div>
@ -40,6 +41,7 @@ import AtomCurrencyInput from '../atoms/AtomCurrencyInput.vue';
import AtomModal from '../atoms/AtomModal.vue';
const modal = ref<InstanceType<typeof AtomModal>>();
const input= ref<InstanceType<typeof AtomCurrencyInput>>();
const amount = ref();
defineProps({
@ -64,7 +66,9 @@ defineProps({
const emit = defineEmits(['submit']);
function handleClose() {
amount.value = '';
if(input.value) {
input.value.reset();
}
}
function handleSubmit() {

View file

@ -61,6 +61,21 @@
</li>
<li class="menu-title !p-0" />
</template>
<template v-if="isBanker">
<li class="pointer-events-none rounded-md bg-info text-warning-content">
<span>
Angemeldet als Banker
<BuildingLibraryIcon class="h-6 w-6" />
</span>
</li>
<li>
<span @click="AuthService.logout()">
<ArrowRightOnRectangleIcon class="h-6 w-6" />
Abmelden
</span>
</li>
<li class="menu-title !p-0" />
</template>
<li
v-for="navigationEntry of navigationEntries"
:class="getNavigationEntryClass(navigationEntry)"
@ -96,11 +111,12 @@ import { useRouter } from 'vue-router';
import { INavigationEntry } from '../../interfaces/navigation-entry.interface';
import { AuthService } from '../../services/auth.service';
import { isDark, toggleDark } from '../../utils/darkMode';
import { ArrowRightOnRectangleIcon, Bars3Icon, ExclamationTriangleIcon, MoonIcon, SunIcon } from '@heroicons/vue/24/outline';
import { ArrowRightOnRectangleIcon, Bars3Icon, BuildingLibraryIcon, ExclamationTriangleIcon, MoonIcon, SunIcon } from '@heroicons/vue/24/outline';
import AtomLogo from '../atoms/AtomLogo.vue';
import AtomSwap from '../atoms/AtomSwap.vue';
const isAdmin = ref(false);
const isBanker = ref(false);
const hideNavigation = ref(false);
const router = useRouter();
const drawerCheckbox = ref();
@ -125,10 +141,12 @@ function getNavigationEntryClass(navigationEntry: INavigationEntry) {
}
useEventBus<boolean>('isAdmin').on(state => (isAdmin.value = state));
useEventBus<boolean>('isBanker').on(state => (isBanker.value = state));
useEventBus<boolean>('hideNavigation').on(state => (hideNavigation.value = state));
onMounted(() => {
isAdmin.value = AuthService.isAdmin();
isBanker.value = AuthService.isBanker();
});
router.afterEach(() => {

View file

@ -1,5 +1,8 @@
<template>
<div>
<MoleculeBankerAuthDialog
v-if="!isBanker && !AuthService.isAdmin()"
/>
<div v-else>
<div
v-if="account || company"
class="hero-content flex-col pt-0 text-center lg:p-6"
@ -94,26 +97,29 @@
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { onKeyStroke, promiseTimeout } from '@vueuse/core';
import { onKeyStroke, promiseTimeout, useEventBus } from '@vueuse/core';
import { RecordSubscription, UnsubscribeFunc } from 'pocketbase';
import { AccountsResponse, CompaniesResponse, SettingsResponse, TransactionsResponse } from '../../types/pocketbase.types';
import { AccountService } from '../../services/account.service';
import { AccountType, BankService, TransactionType } from '../../services/bank.service';
import { AuthService } from '../../services/auth.service';
import { CompanyService } from '../../services/company.service';
import { CurrencyService } from '../../services/currency.service';
import { SettingsService } from '../../services/settings.service';
import { ChevronDownIcon, ChevronUpIcon, XCircleIcon } from '@heroicons/vue/24/outline';
import AtomHeroText from '../atoms/AtomHeroText.vue';
import MoleculeInputModal from '../molecules/MoleculeInputModal.vue';
import MoleculeTransactionTable from '../molecules/MoleculeTransactionTable.vue';
import { SettingsService } from '../../services/settings.service';
import AtomLogo from '../atoms/AtomLogo.vue';
import MoleculeBankerAuthDialog from '../molecules/MoleculeBankerAuthDialog.vue';
const router = useRouter();
const subscription = ref<UnsubscribeFunc>();
const companySubscription = ref<UnsubscribeFunc>();
const isBanker = ref(false);
const account = ref<AccountsResponse|null>();
const company = ref<CompaniesResponse|null>();
const transactions = ref<TransactionsResponse[]>();
@ -132,6 +138,7 @@ if(router.currentRoute.value.query.accountNumber) {
}
onKeyStroke(['e', 'a'], (e) => {
if(!isBanker.value) { return; }
e.preventDefault();
if(depositModal.value?.isOpen() || withdrawModal.value?.isOpen()) {
return;
@ -147,6 +154,7 @@ onKeyStroke(['e', 'a'], (e) => {
});
onKeyStroke(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Enter'], (e) => {
if(!isBanker.value) { return; }
if(depositModal.value?.isOpen() || withdrawModal.value?.isOpen()) {
return;
}
@ -243,15 +251,26 @@ function handleRealtimeUpdates(transactionSubscription: RecordSubscription<Trans
}
onMounted(async () => {
subscription.value = await BankService.subscribeToTransactionChanges(handleRealtimeUpdates);
companySubscription.value = await BankService.subscribeToCompanyTransactionChanges(handleRealtimeUpdates);
settings.value = await SettingsService.getSettings();
isBanker.value = AuthService.isBanker();
});
useEventBus<boolean>('isBanker').on(state => (isBanker.value = state));
onUnmounted(() => {
subscription.value?.();
companySubscription.value?.();
});
watch(isBanker, async (newValue) => {
if(newValue) {
subscription.value = await BankService.subscribeToTransactionChanges(handleRealtimeUpdates);
companySubscription.value = await BankService.subscribeToCompanyTransactionChanges(handleRealtimeUpdates);
settings.value = await SettingsService.getSettings();
} else {
subscription.value?.();
companySubscription.value?.();
}
});
</script>
<style lang="scss" scoped>

View file

@ -1,6 +1,7 @@
import { Admin } from 'pocketbase';
import { PocketbaseService } from './pocketbase.service';
import { useEventBus } from '@vueuse/core';
import { BankersResponse, Collections } from '../types/pocketbase.types';
const API = PocketbaseService.getApi();
@ -29,5 +30,22 @@ export class AuthService {
public static logout(): void {
API.authStore.clear();
useEventBus<boolean>('isAdmin').emit(false);
useEventBus<boolean>('isBanker').emit(false);
}
public static async loginAsBanker(username: string, password: string): Promise<BankersResponse> {
const response = await API.collection(Collections.Bankers).authWithPassword<BankersResponse>(username, password);
if(response) {
useEventBus<boolean>('isBanker').emit(true);
return response.record;
}
throw new Error('Invalid credentials');
}
public static isBanker(): boolean {
if(API.authStore.model && API.authStore.model?.collectionName == Collections.Bankers) {
useEventBus<boolean>('isBanker').emit(true);
return true;
}
return false;
}
}

View file

@ -30,7 +30,7 @@ export class CompanyService {
}
public static isAuthenticated(): boolean {
if(API.authStore.isValid && API.authStore.model) {
return Boolean(API.authStore.model.collectionId);
return API.authStore.model.collectionName == Collections.Companies;
}
this.logout();
return false;

View file

@ -5,6 +5,7 @@
export enum Collections {
Accounts = "accounts",
AccountsList = "accountsList",
Bankers = "bankers",
Companies = "companies",
CompanyTransactions = "companyTransactions",
Settings = "settings",
@ -54,6 +55,8 @@ export type AccountsListRecord<Tbalance = unknown, Tname = unknown> = {
balance?: null | Tbalance
}
export type BankersRecord = never
export type CompaniesRecord = {
accountNumber?: string
name: string
@ -84,6 +87,7 @@ export type TransactionsRecord = {
// Response types include system fields and match responses from the PocketBase API
export type AccountsResponse<Texpand = unknown> = Required<AccountsRecord> & BaseSystemFields<Texpand>
export type AccountsListResponse<Tbalance = unknown, Tname = unknown, Texpand = unknown> = Required<AccountsListRecord<Tbalance, Tname>> & BaseSystemFields<Texpand>
export type BankersResponse<Texpand = unknown> = Required<BankersRecord> & AuthSystemFields<Texpand>
export type CompaniesResponse<Texpand = unknown> = Required<CompaniesRecord> & AuthSystemFields<Texpand>
export type CompanyTransactionsResponse<Texpand = unknown> = Required<CompanyTransactionsRecord> & BaseSystemFields<Texpand>
export type SettingsResponse<Texpand = unknown> = Required<SettingsRecord> & BaseSystemFields<Texpand>
@ -94,6 +98,7 @@ export type TransactionsResponse<Texpand = unknown> = Required<TransactionsRecor
export type CollectionRecords = {
accounts: AccountsRecord
accountsList: AccountsListRecord
bankers: BankersRecord
companies: CompaniesRecord
companyTransactions: CompanyTransactionsRecord
settings: SettingsRecord
@ -103,6 +108,7 @@ export type CollectionRecords = {
export type CollectionResponses = {
accounts: AccountsResponse
accountsList: AccountsListResponse
bankers: BankersResponse
companies: CompaniesResponse
companyTransactions: CompanyTransactionsResponse
settings: SettingsResponse