Merge branch 'master' into apollo-clicktipp-connector

This commit is contained in:
Moriz Wahl 2021-09-16 13:21:38 +02:00
commit eca81883a0
18 changed files with 207 additions and 539 deletions

View File

@ -170,7 +170,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# BUILD NGINX DOCKER IMAGE #############################################
# BUILD NGINX DOCKER IMAGE ###############################################
##########################################################################
- name: nginx | Build `test` image
run: |
@ -182,6 +182,35 @@ jobs:
name: docker-nginx-test
path: /tmp/nginx.tar
##############################################################################
# JOB: LOCALES FRONTEND ######################################################
##############################################################################
locales_frontend:
name: Locales - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v2
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v2
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: frontend | Locales
run: docker run --rm gradido/frontend:test yarn run locales
##############################################################################
# JOB: LINT FRONTEND #########################################################
##############################################################################
@ -206,7 +235,7 @@ jobs:
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LINT FRONTEND ###########################################################
# LINT FRONTEND ##########################################################
##########################################################################
- name: frontend | Lint
run: docker run --rm gradido/frontend:test yarn run lint
@ -316,7 +345,7 @@ jobs:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 60
min_coverage: 66
token: ${{ github.token }}
##############################################################################

View File

@ -39,4 +39,3 @@ git submodule update --recursive --init
## Useful Links
- [Gradido.net](https://gradido.net/)
- [Discord](https://discord.gg/kA3zBAKQDC)

View File

@ -0,0 +1,28 @@
# Authorization and Private Keys
## Keys
For creating transactions ed25519 keys are used for signing.
As long the user is the only controlling the private key he is the only one
how can sign transactions on his behalf.
It is a core concept of all crypto currencies and important for the concept,
that the user has full control over his data.
Usually crypto currencies like bitcoin or iota save the keys on local system,
maybe additional protected with a password which is used to encrypt the keys.
## Gradido
Gradido should be easy to use, so we must offer a solution for everyone not that fit
with computer, as easy to use like paypal.
For that role we have the Login-Server.
It stores the private keys of the user encrypted with there email and password.
Additional it stores the passphrase which can be used to generate the private key,
encryted with server admin public key. So only the server admin can access the keys
with his private key. [not done yet]
It is needed for passwort reset if a user has forgetten his password.
But for the entire concept Login-Server isn't the only way to store the private keys.
For users which has more experience with computer and especially with crypto currencies
it should be a way to keep there private keys by themselfs.
For example a Desktop- or Handy-App which store the keys locally maybe additional encrypted.
Maybe it is possible to use Stronghold from iota for that.
With that the user don't need to use the Login-Server.

View File

@ -0,0 +1,29 @@
# How JWT could be used for authorization with and without Login-Server
## What we need
The only encrypted data in db are the private key.
Every other data could be accessed without login, depending on frontend and backend code.
So we need only a way to prove the backend that we have access to the private key.
## JWT
JWT is perfect for that.
We can use JWT to store the public key of the user as UUID for finding his data in db,
signing it with the private key. So even if the backend is running in multiple instances,
on every request is it possible to check the JWT token, that the signature is signed with
the private key, belonging to the public key.
The only thing the backend cannot do with that is signing a transaction.
That can only be done by the Login-Server or a Desktop or Handy-App storing the private key locally.
With that we have universal way for authorization against the backend.
We could additional store if we like to sign transactions local or with Login-Server and the Login-Server url.
## JWT and Login-Server
Login-Server uses Poco version 1.9.4 but unfortunately Poco only introduces jwt from version 1.10.
And Updating to 1.10 needs some work because some things have changed in Poco 1.10.
## JWT signature algorithms
In JWT standard ed25519 don't seemd to play a role.
We must find out if we can use the ed25519 keys together with one of the signature algorithms
in JWT standard or we must use **crypto_sign_verify_detached** from libsodium even it is nonstandard
to verify signature created with ed25519 keys and libsodiums **crypto_sign_detached** function.

View File

@ -0,0 +1,16 @@
# Session Id Authorization
## Login-Server
With every login, the Login-Server creates a session with a random id,
storing it in memory. For Login email and password are needed.
From email and an additional app-secret (**crypto.app_secret** in Login-Server config) a sha512 hash will be genereted, named **hash512_salt**.
With sodium function *crypto_pwhash* with **hash512_salt** and user password a secret encryption key will be calculated.
*crypto_pwhash* uses argon2 algorithmus to have a CPU hard calculation. Currently it is configured for < 0.5s.
So it is harder to use brute-force attacks to guess the password. Even if someone gets hands on the data saved in db.
With sodium function *crypto_shorthash* a hash will be calculated from the secret encryption key and server crypto key (**crypto.server_key** in Login-Server config, hex encoded, 16 Bytes, 32 Character hex encoded)
and compared against saved hash in db. If they identical user has successfull logged in.
The secret encryption key will be stored in memory together with the user session and client ip from which login call came.
The session_id will be returned.
The session will be hold in memory for 15 minutes default, can be changed in Login-Server config field **session.timeout**

View File

@ -72,6 +72,9 @@ RUN yarn run build
##################################################################################
FROM build as test
# Install Additional Software
RUN apk add --no-cache bash jq
# Run command
CMD /bin/sh -c "yarn run dev"

View File

@ -1,79 +0,0 @@
<template>
<div id="accordion" role="tablist" aria-multiselectable="true" class="accordion">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'collapse',
props: {
animationDuration: {
type: Number,
default: 250,
description: 'Collapse animation duration',
},
multipleActive: {
type: Boolean,
default: true,
description: 'Whether you can have multiple collapse items opened at the same time',
},
activeIndex: {
type: Number,
default: -1,
description: 'Active collapse item index',
},
},
provide() {
return {
animationDuration: this.animationDuration,
multipleActive: this.multipleActive,
addItem: this.addItem,
removeItem: this.removeItem,
deactivateAll: this.deactivateAll,
}
},
data() {
return {
items: [],
}
},
methods: {
addItem(item) {
const index = this.$slots.default.indexOf(item.$vnode)
if (index !== -1) {
this.items.splice(index, 0, item)
}
},
removeItem(item) {
const items = this.items
const index = items.indexOf(item)
if (index > -1) {
items.splice(index, 1)
}
},
deactivateAll() {
this.items.forEach((item) => {
item.active = false
})
},
activateItem() {
if (this.activeIndex !== -1) {
this.items[this.activeIndex].active = true
}
},
},
mounted() {
this.$nextTick(() => {
this.activateItem()
})
},
watch: {
activeIndex() {
this.activateItem()
},
},
}
</script>
<style scoped></style>

View File

@ -1,91 +0,0 @@
<template>
<b-card no-body>
<b-card-header role="tab" class="card-header" :aria-expanded="active">
<a
data-toggle="collapse"
data-parent="#accordion"
:href="`#${itemId}`"
@click.prevent="activate"
:aria-controls="`content-${itemId}`"
>
<slot name="title">{{ title }}</slot>
<i class="tim-icons icon-minimal-down"></i>
</a>
</b-card-header>
<collapse-transition :duration="animationDuration">
<div
v-show="active"
:id="`content-${itemId}`"
role="tabpanel"
:aria-labelledby="title"
class="collapsed"
>
<div class="card-body"><slot></slot></div>
</div>
</collapse-transition>
</b-card>
</template>
<script>
import { CollapseTransition } from 'vue2-transitions'
export default {
name: 'collapse-item',
components: {
CollapseTransition,
},
props: {
title: {
type: String,
default: '',
description: 'Collapse item title',
},
id: String,
},
inject: {
animationDuration: {
default: 250,
},
multipleActive: {
default: false,
},
addItem: {
default: () => {},
},
removeItem: {
default: () => {},
},
deactivateAll: {
default: () => {},
},
},
computed: {
itemId() {
return this.id || this.title
},
},
data() {
return {
active: false,
}
},
methods: {
activate() {
const wasActive = this.active
if (!this.multipleActive) {
this.deactivateAll()
}
this.active = !wasActive
},
},
mounted() {
this.addItem(this)
},
destroyed() {
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
}
this.removeItem(this)
},
}
</script>
<style></style>

View File

@ -3,11 +3,17 @@ import PaginationButtons from './PaginationButtons'
const localVue = global.localVue
const propsData = {
totalRows: 42,
perPage: 12,
value: 1,
}
describe('PaginationButtons', () => {
let wrapper
const Wrapper = () => {
return mount(PaginationButtons, { localVue })
return mount(PaginationButtons, { localVue, propsData })
}
describe('mount', () => {
@ -19,34 +25,20 @@ describe('PaginationButtons', () => {
expect(wrapper.find('div.pagination-buttons').exists()).toBeTruthy()
})
it('has previous page button disabled by default', () => {
expect(wrapper.find('button.previous-page').attributes('disabled')).toBe('disabled')
})
it('has bext page button disabled by default', () => {
expect(wrapper.find('button.next-page').attributes('disabled')).toBe('disabled')
})
it('shows the text "1 / 1" by default"', () => {
expect(wrapper.find('p.text-center').text()).toBe('1 / 1')
})
describe('with active buttons', () => {
beforeEach(async () => {
await wrapper.setProps({
hasNext: true,
hasPrevious: true,
})
})
it('emits show-previous when previous page button is clicked', () => {
wrapper.find('button.previous-page').trigger('click')
expect(wrapper.emitted('show-previous')).toBeTruthy()
})
it('emits show-next when next page button is clicked', () => {
it('emits input next page button is clicked', async () => {
wrapper.find('button.next-page').trigger('click')
expect(wrapper.emitted('show-next')).toBeTruthy()
await wrapper.vm.$nextTick()
expect(wrapper.emitted().input[0]).toEqual([2])
})
it('emits input when previous page button is clicked', async () => {
wrapper.setProps({ value: 2 })
wrapper.setData({ currentValue: 2 })
await wrapper.vm.$nextTick()
wrapper.find('button.previous-page').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.emitted().input[0]).toEqual([1])
})
})
})

View File

@ -1,16 +1,16 @@
<template>
<div class="pagination-buttons">
<div class="pagination-buttons" v-if="totalRows > perPage">
<b-row class="m-4">
<b-col class="text-right">
<b-button class="previous-page" :disabled="!hasPrevious" @click="$emit('show-previous')">
<b-button class="previous-page" :disabled="!hasPrevious" @click="currentValue--">
<b-icon icon="chevron-left" variant="primary"></b-icon>
</b-button>
</b-col>
<b-col cols="3">
<p class="text-center pt-2">{{ currentPage }} / {{ totalPages }}</p>
<p class="text-center pt-2">{{ value }} / {{ totalPages }}</p>
</b-col>
<b-col>
<b-button class="next-page" :disabled="!hasNext" @click="$emit('show-next')">
<b-button class="next-page" :disabled="!hasNext" @click="currentValue++">
<b-icon icon="chevron-right" variant="primary"></b-icon>
</b-button>
</b-col>
@ -21,10 +21,33 @@
export default {
name: 'PaginationButtons',
props: {
hasNext: { type: Boolean, default: false },
hasPrevious: { type: Boolean, default: false },
totalPages: { type: Number, default: 1 },
currentPage: { type: Number, default: 1 },
totalRows: { required: true },
perPage: { type: Number, required: true },
value: { type: Number, required: true },
},
data() {
return {
currentValue: { type: Number, default: 1 },
}
},
computed: {
hasNext() {
return this.value * this.perPage < this.totalRows
},
hasPrevious() {
return this.value > 1
},
totalPages() {
return Math.ceil(this.totalRows / this.perPage)
},
},
created() {
this.currentValue = this.value
},
watch: {
currentValue() {
if (this.currentValue !== this.value) this.$emit('input', this.currentValue)
},
},
}
</script>

View File

@ -1,8 +1,5 @@
import NavbarToggleButton from './Navbar/NavbarToggleButton'
import Collapse from './Collapse/Collapse.vue'
import CollapseItem from './Collapse/CollapseItem.vue'
import SidebarPlugin from './SidebarPlugin'
export { SidebarPlugin, NavbarToggleButton, Collapse, CollapseItem }
export { SidebarPlugin, NavbarToggleButton }

View File

@ -6,6 +6,8 @@ sendMock.mockResolvedValue('success')
const localVue = global.localVue
window.scrollTo = jest.fn()
describe('AccountOverview', () => {
let wrapper

View File

@ -1,183 +0,0 @@
<template>
<div class="pt-4 pb-4">
<b-tabs content-class="mt-3" class="display-4" fill>
<b-tab :title="names.thisMonth" active>
<b-row>
<b-col lg="3">
<b-input :label="$t('communitys.form.hours')">
<b-form-input
type="number"
size="lg"
placeholder="23"
style="font-size: xx-large; padding-left: 5px"
/>
</b-input>
<b-input :label="$t('communitys.form.date_period')">
<flat-pickr
class="form-control"
v-model="date"
:config="config"
style="font-size: 0.5em; padding-left: 5px"
></flat-pickr>
</b-input>
</b-col>
<b-col lg="9">
<b-input :label="$t('communitys.form.hours_report')">
<textarea
class="form-control"
rows="5"
@focus="textFocus"
style="font-size: x-large; padding-left: 20px"
></textarea>
</b-input>
</b-col>
</b-row>
<b-row>
<div ref="mydiv"></div>
</b-row>
<b-row>
<b-col md="6">
<b-button @click.prevent="newWorkForm" variant="warning">
+ {{ $t('communitys.form.more_hours') }}
</b-button>
</b-col>
<b-col md="6" class="text-right">
<b-button variant="success" @click.prevent="submitForm2">
{{ $t('communitys.form.submit') }}
</b-button>
</b-col>
</b-row>
</b-tab>
<b-tab :title="names.lastMonth"></b-tab>
<b-tab :title="names.beforLastMonth"></b-tab>
</b-tabs>
</div>
</template>
<script>
export default {
name: 'GDDAddWork2',
data() {
return {
date: null,
config: {
altInput: false,
dateFormat: 'd-m-Y',
minDate: this.$moment().startOf('month').format('DD.MM.YYYY'),
maxDate: this.$moment().format('DD.MM.YYYY'),
mode: 'range',
},
index: 0,
form: [],
stundenSumme: 0,
messages: [],
submitted: false,
days: {
thisMonth: this.$moment().month(this.$moment().month()).daysInMonth(),
lastMonth: this.$moment()
.month(this.$moment().month() - 1)
.daysInMonth(),
beforLastMonth: this.$moment()
.month(this.$moment().month() - 2)
.daysInMonth(),
},
names: {
thisMonth: this.$moment().month(this.$moment().month()).format('MMMM'),
lastMonth: this.$moment()
.month(this.$moment().month() - 1)
.format('MMMM'),
beforLastMonth: this.$moment()
.month(this.$moment().month() - 2)
.format('MMMM'),
},
formular: null,
}
},
created() {},
watch: {
$form: function () {
this.stunden(this.form)
},
},
mounted() {},
methods: {
stunden(hour, i, mon) {
let n = 0
this.stundenSumme = 0
for (n; n < this.form.length; n++) {
if (this.form[n] > 0) {
this.stundenSumme += parseInt(this.form[n])
}
}
this.messages.push({
id: this.index,
MonthsNumber: mon,
DaysNumber: i,
HoursNumber: hour,
DestinationText: '',
TextDecoded: '',
})
this.index++
},
addNewMessage: function () {
this.messages.push({
DaysNumber: '',
TextDecoded: '',
})
},
deleteNewMessage: function (event) {
this.form.splice(event, null)
this.messages.splice(this.index, 1)
this.index--
},
submitForm: function (e) {
// console.log('submitForm')
this.messages = [{ DaysNumber: '', TextDecoded: '' }]
this.submitted = true
},
textFocus() {
// console.log('textFocus TODO')
},
newWorkForm() {
this.formular = `
<b-col lg="3">
<b-input label="Stunden">
<b-form-input
type="number"
size="lg"
placeholder="0"
style="font-size: xx-large; padding-left: 20px"
/>
</b-input>
<b-input label="Datum / Zeitraum">
<flat-pickr
class="form-control"
v-model="date"
:config="config"
style="font-size: xx-large; padding-left: 20px"
></flat-pickr>
</b-input>
</b-col>
<b-col lg="9">
<b-input label="Arbeitsreport">
<textarea
class="form-control"
rows="5"
@focus="textFocus"
style="font-size: x-large; padding-left: 20px"
></textarea>
</b-input>
</b-col>
`
// console.log('newWorkForm TODO')
const myElement = this.$refs.mydiv
myElement.append(this.formular)
this.$compile(myElement)
this.formular = null
},
},
}
</script>

View File

@ -84,13 +84,10 @@
</div>
</div>
<pagination-buttons
v-if="showPagination && transactionCount > pageSize"
:has-next="hasNext"
:has-previous="hasPrevious"
:total-pages="totalPages"
:current-page="currentPage"
@show-next="showNext"
@show-previous="showPrevious"
v-if="showPagination"
v-model="currentPage"
:per-page="pageSize"
:total-rows="transactionCount"
></pagination-buttons>
<div v-if="transactions.length === 0" class="mt-4 text-center">
<span>{{ $t('transaction.nullTransactions') }}</span>
@ -128,29 +125,13 @@ export default {
transactionCount: { type: Number, default: 0 },
showPagination: { type: Boolean, default: false },
},
watch: {
timestamp: {
immediate: true,
handler: 'updateTransactions',
},
},
computed: {
hasNext() {
return this.currentPage * this.pageSize < this.transactionCount
},
hasPrevious() {
return this.currentPage > 1
},
totalPages() {
return Math.ceil(this.transactionCount / this.pageSize)
},
},
methods: {
updateTransactions() {
this.$emit('update-transactions', {
firstPage: this.currentPage,
items: this.pageSize,
})
window.scrollTo(0, 0)
},
getProperties(givenType) {
const type = iconsByType[givenType]
@ -165,15 +146,14 @@ export default {
throwError(msg) {
throw new Error(msg)
},
showNext() {
this.currentPage++
},
watch: {
currentPage() {
this.updateTransactions()
window.scrollTo(0, 0)
},
showPrevious() {
this.currentPage--
this.updateTransactions()
window.scrollTo(0, 0)
timestamp: {
immediate: true,
handler: 'updateTransactions',
},
},
}

View File

@ -1,83 +0,0 @@
<template>
<div>
<b-list-group>
<b-list-group-item v-for="item in items" :key="item.id">
<div class="d-flex w-100 justify-content-between" @click="toogle(item)">
<b-icon
v-if="item.status == 'submitted'"
icon="clock-history"
class="m-1"
font-scale="2"
style="color: orange"
></b-icon>
<b-icon v-else icon="check2-all" class="m-1" font-scale="2" style="color: green"></b-icon>
<h2 class="text-muted">
<small>{{ item.datel }}</small>
- {{ item.text }}
</h2>
</div>
</b-list-group-item>
</b-list-group>
<hr />
<b-icon icon="clock-history" class="m-1" font-scale="2" style="color: orange"></b-icon>
Wartet auf Bestätigung |
<b-icon icon="check2-all" class="m-1" font-scale="2" style="color: green"></b-icon>
bestätigt
</div>
</template>
<script>
export default {
name: 'GddWorkTable',
data() {
return {
form: [],
items: [
{
id: 1,
text: 'Zwei Säcke Plastikmüll im Wald gesammelt',
datel: '12.12.2020 14:04',
status: 'submitted',
},
{
id: 2,
text: 'Frau Schmidt bei der Gartenarbeit geholfen.',
datel: '22.06.2020 22:23',
status: 'submitted',
},
{
id: 3,
text: 'Ein online Kurs für nachhaltiges Mülltrennen erstellt',
datel: '15.04.2020 12:55',
status: 'confirmed',
},
{
id: 4,
text: 'Gradido bei meinen Freunden vorgestellt',
datel: '10.03.2020 18:20',
status: 'confirmed',
},
],
}
},
methods: {
rowClass(item, type) {
if (!item || type !== 'row') return
if (item.status === 'received') return 'table-success'
if (item.status === 'sent') return 'table-warning'
if (item.status === 'earned') return 'table-primary'
},
toogle(item) {
// eslint-disable-next-line no-unused-vars
const temp =
'<b-collapse visible v-bind:id="item.id">xxx <small class="text-muted">porta</small></b-collapse>'
},
},
}
</script>
<style>
.el-table .cell {
padding-left: 0px;
padding-right: 0px;
}
</style>

View File

@ -46,6 +46,9 @@ const apolloMock = jest.fn().mockResolvedValue({
})
const toastErrorMock = jest.fn()
const windowScrollToMock = jest.fn()
window.scrollTo = windowScrollToMock
describe('GdtTransactionList', () => {
let wrapper
@ -90,6 +93,10 @@ describe('GdtTransactionList', () => {
}),
)
})
it('scrolls to (0, 0) after API call', () => {
expect(windowScrollToMock).toBeCalledWith(0, 0)
})
})
describe('server returns error', () => {
@ -105,5 +112,21 @@ describe('GdtTransactionList', () => {
expect(toastErrorMock).toBeCalledWith('Ouch!')
})
})
describe('change of currentPage', () => {
it('calls the API after currentPage changes', async () => {
jest.clearAllMocks()
wrapper.setData({ currentPage: 2 })
await wrapper.vm.$nextTick()
expect(apolloMock).toBeCalledWith(
expect.objectContaining({
variables: {
currentPage: 2,
pageSize: 25,
},
}),
)
})
})
})
})

View File

@ -28,13 +28,9 @@
</div>
</div>
<pagination-buttons
v-if="transactionGdtCount > pageSize"
:has-next="hasNext"
:has-previous="hasPrevious"
:total-pages="totalPages"
:current-page="currentPage"
@show-next="showNext"
@show-previous="showPrevious"
v-model="currentPage"
:per-page="pageSize"
:total-rows="transactionGdtCount"
></pagination-buttons>
</div>
</template>
@ -52,23 +48,12 @@ export default {
},
data() {
return {
transactionsGdt: { default: () => [] },
transactionsGdt: [],
transactionGdtCount: { type: Number, default: 0 },
currentPage: 1,
pageSize: 25,
}
},
computed: {
hasNext() {
return this.currentPage * this.pageSize < this.transactionGdtCount
},
hasPrevious() {
return this.currentPage > 1
},
totalPages() {
return Math.ceil(this.transactionGdtCount / this.pageSize)
},
},
methods: {
async updateGdt() {
this.$apollo
@ -85,25 +70,21 @@ export default {
} = result
this.transactionsGdt = listGDTEntries.gdtEntries
this.transactionGdtCount = listGDTEntries.count
window.scrollTo(0, 0)
})
.catch((error) => {
this.$toasted.error(error.message)
})
},
showNext() {
this.currentPage++
this.updateGdt()
window.scrollTo(0, 0)
},
showPrevious() {
this.currentPage--
this.updateGdt()
window.scrollTo(0, 0)
},
},
mounted() {
this.updateGdt()
},
watch: {
currentPage() {
this.updateGdt()
},
},
}
</script>
<style>

View File

@ -3,6 +3,8 @@ import UserProfileTransactionList from './UserProfileTransactionList'
const localVue = global.localVue
window.scrollTo = jest.fn()
describe('UserProfileTransactionList', () => {
let wrapper