Merge remote-tracking branch 'origin/master' into

3263-feature-gms-publish-user-backend-update-user-settings
This commit is contained in:
Claus-Peter Huebner 2024-02-07 23:54:59 +01:00
commit 64d192f752
48 changed files with 549 additions and 181 deletions

View File

@ -27,7 +27,7 @@ DLT_CONNECTOR_URL=http://localhost:6010
# Community
COMMUNITY_NAME=Gradido Entwicklung
COMMUNITY_URL=http://localhost/
COMMUNITY_URL=http://localhost
COMMUNITY_REGISTER_PATH=/register
COMMUNITY_REDEEM_PATH=/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_PATH=/redeem/CL-{code}

View File

@ -12,7 +12,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0081-introduce_gms_registration',
DB_VERSION: '0082-introduce_gms_registration',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info

View File

@ -3,11 +3,11 @@ import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export class UserArgs {
@Field({ nullable: false })
@Field()
@IsString()
identifier: string
@Field({ nullable: true })
@Field()
@IsString()
communityIdentifier?: string
communityIdentifier: string
}

View File

@ -10,6 +10,9 @@ export class User {
this.id = user.id
this.foreign = user.foreign
this.communityUuid = user.communityUuid
if (user.community) {
this.communityName = user.community.name
}
this.gradidoID = user.gradidoID
this.alias = user.alias
if (user.emailContact) {

View File

@ -144,7 +144,11 @@ describe('send coins', () => {
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('No user with this credentials', 'wrong@email.com')
expect(logger.error).toBeCalledWith(
'No user with this credentials',
'wrong@email.com',
homeCom.communityUuid,
)
})
describe('deleted recipient', () => {
@ -167,13 +171,17 @@ describe('send coins', () => {
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user to given contact')],
errors: [new GraphQLError('No user with this credentials')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('No user to given contact', 'stephen@hawking.uk')
expect(logger.error).toBeCalledWith(
'No user with this credentials',
'stephen@hawking.uk',
homeCom.communityUuid,
)
})
})
@ -206,6 +214,7 @@ describe('send coins', () => {
expect(logger.error).toBeCalledWith(
'No user with this credentials',
'garrick@ollivander.com',
homeCom.communityUuid,
)
})
})

View File

@ -432,7 +432,7 @@ export class TransactionResolver {
const senderUser = getUser(context)
if (!recipientCommunityIdentifier || (await isHomeCommunity(recipientCommunityIdentifier))) {
// processing sendCoins within sender and recepient are both in home community
// processing sendCoins within sender and recipient are both in home community
const recipientUser = await findUserByIdentifier(
recipientIdentifier,
recipientCommunityIdentifier,

View File

@ -2679,6 +2679,7 @@ describe('UserResolver', () => {
query: userQuery,
variables: {
identifier: 'identifier',
communityIdentifier: 'community identifier',
},
}),
).resolves.toEqual(
@ -2767,13 +2768,11 @@ describe('UserResolver', () => {
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError('Found user to given contact, but belongs to other community'),
],
errors: [new GraphQLError('No user with this credentials')],
}),
)
expect(logger.error).toBeCalledWith(
'Found user to given contact, but belongs to other community',
'No user with this credentials',
'bibi@bloxberg.de',
foreignCom1.communityUuid,
)

View File

@ -66,7 +66,7 @@ import random from 'random-bigint'
import { randombytes_random } from 'sodium-native'
import { FULL_CREATION_AVAILABLE } from './const/const'
import { getCommunityName, getHomeCommunity } from './util/communities'
import { getHomeCommunity } from './util/communities'
import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier'
import { findUsers } from './util/findUsers'
@ -851,11 +851,6 @@ export class UserResolver {
): Promise<User> {
const foundDbUser = await findUserByIdentifier(identifier, communityIdentifier)
const modelUser = new User(foundDbUser)
if (!foundDbUser.communityUuid) {
modelUser.communityName = (await Promise.resolve(getHomeCommunity())).name
} else {
modelUser.communityName = await getCommunityName(foundDbUser.communityUuid)
}
return modelUser
}
}

View File

@ -6,6 +6,7 @@ import { Community as DbCommunity } from '@entity/Community'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { Decimal } from 'decimal.js-light'
import { GraphQLError } from 'graphql'
import { v4 as uuidv4 } from 'uuid'
import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers'
@ -54,7 +55,7 @@ describe('semaphore', () => {
beforeAll(async () => {
const now = new Date()
homeCom = DbCommunity.create()
homeCom.communityUuid = 'homeCom-UUID'
homeCom.communityUuid = uuidv4()
homeCom.creationDate = new Date('2000-01-01')
homeCom.description = 'homeCom description'
homeCom.foreign = false

View File

@ -1,3 +1,5 @@
import { FindOptionsWhere } from '@dbTools/typeorm'
import { Community } from '@entity/Community'
import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact'
import { validate, version } from 'uuid'
@ -6,15 +8,26 @@ import { LogError } from '@/server/LogError'
import { VALID_ALIAS_REGEX } from './validateAlias'
/**
*
* @param identifier could be gradidoID, alias or email of user
* @param communityIdentifier could be uuid or name of community
* @returns
*/
export const findUserByIdentifier = async (
identifier: string,
communityIdentifier?: string,
communityIdentifier: string,
): Promise<DbUser> => {
let user: DbUser | null
const communityWhere: FindOptionsWhere<Community> =
validate(communityIdentifier) && version(communityIdentifier) === 4
? { communityUuid: communityIdentifier }
: { name: communityIdentifier }
if (validate(identifier) && version(identifier) === 4) {
user = await DbUser.findOne({
where: { gradidoID: identifier, communityUuid: communityIdentifier },
relations: ['emailContact'],
where: { gradidoID: identifier, community: communityWhere },
relations: ['emailContact', 'community'],
})
if (!user) {
throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier)
@ -24,28 +37,21 @@ export const findUserByIdentifier = async (
where: {
email: identifier,
emailChecked: true,
user: {
community: communityWhere,
},
},
relations: ['user'],
relations: { user: { community: true } },
})
if (!userContact) {
throw new LogError('No user with this credentials', identifier)
}
if (!userContact.user) {
throw new LogError('No user to given contact', identifier)
}
if (userContact.user.communityUuid !== communityIdentifier) {
throw new LogError(
'Found user to given contact, but belongs to other community',
identifier,
communityIdentifier,
)
throw new LogError('No user with this credentials', identifier, communityIdentifier)
}
user = userContact.user
user.emailContact = userContact
} else if (VALID_ALIAS_REGEX.exec(identifier)) {
user = await DbUser.findOne({
where: { alias: identifier, communityUuid: communityIdentifier },
relations: ['emailContact'],
where: { alias: identifier, community: communityWhere },
relations: ['emailContact', 'community'],
})
if (!user) {
throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier)

View File

@ -0,0 +1,94 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { Connection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { User as DbUser } from '@entity/User'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { cleanDB, testEnvironment } from '@test/helpers'
import { writeHomeCommunityEntry } from '@/seeds/community'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { findUserByIdentifier } from './findUserByIdentifier'
let con: Connection
let testEnv: {
mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query']
con: Connection
}
beforeAll(async () => {
testEnv = await testEnvironment()
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('graphql/resolver/util/findUserByIdentifier', () => {
let homeCom: DbCommunity
let communityUuid: string
let communityName: string
let userBibi: DbUser
beforeAll(async () => {
homeCom = await writeHomeCommunityEntry()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
communityUuid = homeCom.communityUuid!
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
communityName = homeCom.communityUuid!
userBibi = await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bobBaumeister)
})
describe('communityIdentifier is community uuid', () => {
it('userIdentifier is gradido id', async () => {
const user = await findUserByIdentifier(userBibi.gradidoID, communityUuid)
user.userRoles = []
expect(user).toMatchObject(userBibi)
})
it('userIdentifier is alias', async () => {
const user = await findUserByIdentifier(userBibi.alias, communityUuid)
user.userRoles = []
expect(user).toMatchObject(userBibi)
})
it('userIdentifier is email', async () => {
const user = await findUserByIdentifier(userBibi.emailContact.email, communityUuid)
user.userRoles = []
expect(user).toMatchObject(userBibi)
})
})
describe('communityIdentifier is community name', () => {
it('userIdentifier is gradido id', async () => {
const user = await findUserByIdentifier(userBibi.gradidoID, communityName)
user.userRoles = []
expect(user).toMatchObject(userBibi)
})
it('userIdentifier is alias', async () => {
const user = await findUserByIdentifier(userBibi.alias, communityName)
user.userRoles = []
expect(user).toMatchObject(userBibi)
})
it('userIdentifier is email', async () => {
const user = await findUserByIdentifier(userBibi.emailContact.email, communityName)
user.userRoles = []
expect(user).toMatchObject(userBibi)
})
})
})

View File

@ -387,7 +387,7 @@ export const adminListContributionMessages = gql`
`
export const user = gql`
query ($identifier: String!, $communityIdentifier: String) {
query ($identifier: String!, $communityIdentifier: String!) {
user(identifier: $identifier, communityIdentifier: $communityIdentifier) {
firstName
lastName

View File

@ -4,6 +4,7 @@ export const bibiBloxberg: UserInterface = {
email: 'bibi@bloxberg.de',
firstName: 'Bibi',
lastName: 'Bloxberg',
alias: 'BBB',
// description: 'Hex Hex',
emailChecked: true,
language: 'de',

View File

@ -50,6 +50,7 @@ const communityDbUser: dbUser = {
},
foreign: false,
communityUuid: '55555555-4444-4333-2222-11111111',
community: null,
gmsPublishName: 0,
gmsAllowed: false,
location: null,

View File

@ -0,0 +1,70 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
JoinColumn,
} from 'typeorm'
import { User } from '../User'
@Entity('communities')
export class Community extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
foreign: boolean
@Column({ name: 'url', length: 255, nullable: false })
url: string
@Column({ name: 'public_key', type: 'binary', length: 32, nullable: false })
publicKey: Buffer
@Column({ name: 'private_key', type: 'binary', length: 64, nullable: true })
privateKey: Buffer | null
@Column({
name: 'community_uuid',
type: 'char',
length: 36,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
communityUuid: string | null
@Column({ name: 'authenticated_at', type: 'datetime', nullable: true })
authenticatedAt: Date | null
@Column({ name: 'name', type: 'varchar', length: 40, nullable: true })
name: string | null
@Column({ name: 'description', type: 'varchar', length: 255, nullable: true })
description: string | null
@CreateDateColumn({ name: 'creation_date', type: 'datetime', nullable: true })
creationDate: Date | null
@CreateDateColumn({
name: 'created_at',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP(3)',
nullable: false,
})
createdAt: Date
@UpdateDateColumn({
name: 'updated_at',
type: 'datetime',
onUpdate: 'CURRENT_TIMESTAMP(3)',
nullable: true,
})
updatedAt: Date | null
@OneToMany(() => User, (user) => user.community)
@JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' })
users: User[]
}

View File

@ -0,0 +1,138 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
OneToMany,
JoinColumn,
OneToOne,
ManyToOne,
} from 'typeorm'
import { Contribution } from '../Contribution'
import { ContributionMessage } from '../ContributionMessage'
import { UserContact } from '../UserContact'
import { UserRole } from '../UserRole'
import { Community } from '../Community'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ type: 'bool', default: false })
foreign: boolean
@Column({
name: 'gradido_id',
length: 36,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
gradidoID: string
@Column({
name: 'community_uuid',
type: 'char',
length: 36,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
communityUuid: string
@ManyToOne(() => Community, (community) => community.users)
@JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' })
community: Community | null
@Column({
name: 'alias',
length: 20,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
alias: string
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
@JoinColumn({ name: 'email_id' })
emailContact: UserContact
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
emailId: number | null
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
createdAt: Date
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
deletedAt: Date | null
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({
name: 'password_encryption_type',
type: 'int',
unsigned: true,
nullable: false,
default: 0,
})
passwordEncryptionType: number
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ type: 'bool', default: false })
hideAmountGDD: boolean
@Column({ type: 'bool', default: false })
hideAmountGDT: boolean
@OneToMany(() => UserRole, (userRole) => userRole.user)
@JoinColumn({ name: 'user_id' })
userRoles: UserRole[]
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
referrerId?: number | null
@Column({
name: 'contribution_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
contributionLinkId?: number | null
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@OneToMany(() => Contribution, (contribution) => contribution.user)
@JoinColumn({ name: 'user_id' })
contributions?: Contribution[]
@OneToMany(() => ContributionMessage, (message) => message.user)
@JoinColumn({ name: 'user_id' })
messages?: ContributionMessage[]
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
@JoinColumn({ name: 'user_id' })
userContacts?: UserContact[]
}

View File

@ -1 +1,5 @@
<<<<<<< HEAD
export { Community } from './0081-introduce_gms_registration/Community'
=======
export { Community } from './0081-user_join_community/Community'
>>>>>>> refs/remotes/origin/master

View File

@ -1 +1,5 @@
<<<<<<< HEAD
export { User } from './0081-introduce_gms_registration/User'
=======
export { User } from './0081-user_join_community/User'
>>>>>>> refs/remotes/origin/master

View File

@ -0,0 +1,11 @@
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE users MODIFY community_uuid VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE users MODIFY community_uuid VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
)
}

View File

@ -1,3 +1,4 @@
limit_req_zone $binary_remote_addr zone=frontend:20m rate=5r/s;
limit_req_zone $binary_remote_addr zone=backend:25m rate=15r/s;
limit_req_zone $binary_remote_addr zone=api:5m rate=30r/s;
limit_req_zone $binary_remote_addr zone=api:5m rate=30r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;

View File

@ -1,3 +1,5 @@
include /etc/nginx/common/limit_requests.conf;
server {
if ($host = $COMMUNITY_HOST) {
return 301 https://$host$request_uri;
@ -21,7 +23,6 @@ server {
include /etc/nginx/common/protect.conf;
include /etc/nginx/common/protect_add_header.conf;
include /etc/nginx/common/limit_requests.conf;
# protect from slow loris
client_body_timeout 10s;

View File

@ -1,3 +1,5 @@
include /etc/nginx/common/limit_requests.conf;
server {
server_name $COMMUNITY_HOST;
@ -6,7 +8,6 @@ server {
include /etc/nginx/common/protect.conf;
include /etc/nginx/common/protect_add_header.conf;
include /etc/nginx/common/limit_requests.conf;
# protect from slow loris
client_body_timeout 10s;

View File

@ -1,3 +1,4 @@
include /etc/nginx/common/limit_requests.conf;
server {
if ($host = $COMMUNITY_HOST) {
@ -21,7 +22,6 @@ server {
include /etc/nginx/common/protect.conf;
include /etc/nginx/common/protect_add_header.conf;
include /etc/nginx/common/limit_requests.conf;
# protect from slow loris
client_body_timeout 10s;

View File

@ -1,3 +1,4 @@
include /etc/nginx/common/limit_requests.conf;
server {
server_name $COMMUNITY_HOST;
@ -6,7 +7,6 @@ server {
include /etc/nginx/common/protect.conf;
include /etc/nginx/common/protect_add_header.conf;
include /etc/nginx/common/limit_requests.conf;
# protect from slow loris
client_body_timeout 10s;

View File

@ -9,6 +9,7 @@ users:
packages:
- fail2ban
- python3-systemd
- ufw
- git
- mariadb-server

View File

@ -80,6 +80,14 @@ expect eof
")
echo "$SECURE_MYSQL"
# Configure fail2ban, seems to not run out of the box on Debian 12
echo -e "[sshd]\nbackend = systemd" | tee /etc/fail2ban/jail.d/sshd.conf
# enable nginx-limit-req filter to block also user which exceed nginx request limiter
echo -e "[nginx-limit-req]\nenabled = true\nlogpath = $SCRIPT_PATH/log/nginx-error.*.log" | tee /etc/fail2ban/jail.d/nginx-limit-req.conf
# enable nginx bad request filter
echo -e "[nginx-bad-request]\nenabled = true\nlogpath = $SCRIPT_PATH/log/nginx-error.*.log" | tee /etc/fail2ban/jail.d/nginx-bad-request.conf
systemctl restart fail2ban
# Configure nginx
rm /etc/nginx/sites-enabled/default
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/nginx/sites-available/gradido.conf.template > $SCRIPT_PATH/nginx/sites-available/gradido.conf

View File

@ -4,7 +4,7 @@ import dotenv from 'dotenv'
dotenv.config()
const constants = {
DB_VERSION: '0081-introduce_gms_registration',
DB_VERSION: '0082-introduce_gms_registration',
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',

View File

@ -10,7 +10,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0081-introduce_gms_registration',
DB_VERSION: '0082-introduce_gms_registration',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info

View File

@ -4,7 +4,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.{js,vue}', '!**/node_modules/**', '!**/?(*.)+(spec|test).js?(x)'],
coverageThreshold: {
global: {
lines: 95,
lines: 94,
},
},
moduleFileExtensions: [

View File

@ -1,47 +1,37 @@
[
{
"locale": "de",
"date": "30.10.2023",
"text": "Gradido wird dezentral (Beta)",
"url": "/send",
"extra": "Gradido-Communities können nun ihre eigenen dezentralen Gradido-Server betreiben. Transaktionen zwischen den Servern sind möglich. Im Senden-Dialog findest Du ein Auswahl-Menü mit den verfügbaren Communities, um das Ziel für den Empfänger auszuwählen. Bitte beachte, dass diese Funktion sich noch in der Beta-Testphase befindet und die Communities erst nach und nach hinzugefügt werden.",
"extra2": "Wenn Ihr als Community Euren eigenen Gradido-Server betreiben wollt, schreibt uns bitte eine E-Mail an ",
"email": "support@gradido.net"
"text": "Gradido-Kreise Gemeinsam mit Freunden die Zukunft gestalten",
"button": "Mehr erfahren",
"url": "https://gradido.net/de/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten",
"extra": "Ganz gleich, ob Ihr bereits einer Gruppe zugehörig seid oder ob Ihr Euch über Gradido gefunden habt wenn Ihr gemeinsam Gradido nutzen wollt, braucht Ihr nicht gleich einen eigenen Gradido-Server."
},
{
"locale": "en",
"date": "30.10.2023",
"text": "Gradido becomes decentralized (Beta)",
"url": "/send",
"extra": "Gradido communities can now run their own decentralized Gradido servers. Transactions between the servers are possible. In the send dialog you will find a dropdown menu with the available communities to select the destination for the receiver. Please note that this feature is still in beta testing and communities will be added gradually.",
"extra2": "If you want to run your own Gradido server as a community, please send us an email to ",
"email": "support@gradido.net"
"text": "Gradido circles - Shaping the future together with friends",
"button": "Learn more",
"url": "https://gradido.net/en/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
"extra": "No matter whether you already belong to a group or whether you found each other via Gradido - if you want to use Gradido together, you don't need your own Gradido server."
},
{
"locale": "fr",
"date": "30.10.2023",
"text": "Gradido devient décentralisé (Beta)",
"url": "/send",
"extra": "Les communautés Gradido peuvent désormais gérer leurs propres serveurs Gradido décentralisés. Les transactions entre les serveurs sont possibles. Dans la boîte de dialogue d'envoi, tu trouveras un menu de sélection avec les communautés disponibles pour choisir la destination du destinataire. Veuillez noter que cette fonction est encore en phase de test bêta et que les communautés ne seront ajoutées qu'au fur et à mesure.",
"extra2": "Si vous souhaitez exploiter votre propre serveur Gradido en tant que communauté, veuillez nous envoyer un e-mail à ",
"email": "support@gradido.net"
"text": "Cercles Gradido - Construire l'avenir ensemble avec des amis ",
"button": "En savoir plus",
"url": "https://gradido.net/fr/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
"extra": "Que vous fassiez déjà partie d'un groupe ou que vous vous soyez trouvés par le biais de Gradido, si vous voulez utiliser Gradido ensemble, vous n'avez pas besoin de votre propre serveur Gradido."
},
{
"locale": "es",
"date": "30.10.2023",
"text": "Gradido se descentraliza (Beta)",
"url": "/send",
"extra": "Las comunidades de Gradido ya pueden gestionar sus propios servidores descentralizados de Gradido. Las transacciones entre los servidores son posibles. En el diálogo de envío encontrarás un menú desplegable con las comunidades disponibles para seleccionar el destino del destinatario. Ten en cuenta que esta función aún está en fase de pruebas beta y que las comunidades se irán añadiendo poco a poco.",
"extra2": "Si quieres gestionar tu propio servidor Gradido como comunidad, envíanos un correo electrónico a ",
"email": "support@gradido.net"
"text": "Círculos Gradido - Forjar el futuro entre amigos ",
"button": "Más información",
"url": "https://gradido.net/es/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
"extra": "No importa si ya pertenecéis a un grupo o si os habéis encontrado a través de Gradido: si queréis utilizar Gradido juntos, no necesitáis vuestro propio servidor Gradido."
},
{
"locale": "nl",
"date": "30.10.2023",
"text": "Gradido wordt gedecentraliseerd (Beta)",
"url": "/send",
"extra": "Gradido-gemeenschappen kunnen nu hun eigen gedecentraliseerde Gradido-servers beheren. Transacties tussen de servers zijn mogelijk. In het verzenddialoogvenster vind je een vervolgkeuzemenu met de beschikbare communities om de bestemming voor de ontvanger te selecteren. Houd er rekening mee dat deze functie zich nog in de beta-testfase bevindt en dat de communities beetje bij beetje zullen worden toegevoegd.",
"extra2": "Als je je eigen Gradido server als community wilt gebruiken, stuur ons dan een e-mail naar ",
"email": "support@gradido.net"
"text": "Gradidokringen - Samen met vrienden de toekomst vormgeven",
"button": "Meer informatie",
"url": "https://gradido.net/nl/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/",
"extra": "Het maakt niet uit of je al tot een groep behoort of dat je elkaar via Gradido hebt gevonden - als je Gradido samen wilt gebruiken, heb je geen eigen Gradido-server nodig."
}
]

View File

@ -1,16 +1,23 @@
<template>
<div class="community-switch">
<b-dropdown no-flip :text="value.name">
<b-dropdown-item
v-for="community in communities"
@click.prevent="updateCommunity(community)"
:key="community.id"
:title="community.description"
:active="value.uuid === community.uuid"
>
{{ community.name }}
</b-dropdown-item>
</b-dropdown>
<div v-if="!validCommunityIdentifier">
<b-dropdown no-flip :text="value.name">
<b-dropdown-item
v-for="community in communities"
@click.prevent="updateCommunity(community)"
:key="community.id"
:title="community.description"
:active="value.uuid === community.uuid"
>
{{ community.name }}
</b-dropdown-item>
</b-dropdown>
</div>
<div v-else class="mb-4 mt-2">
<b-row>
<b-col class="font-weight-bold" :title="value.description">{{ value.name }}</b-col>
</b-row>
</div>
</div>
</template>
<script>
@ -26,6 +33,7 @@ export default {
data() {
return {
communities: [],
validCommunityIdentifier: false,
}
},
methods: {
@ -33,6 +41,27 @@ export default {
this.$emit('input', community)
},
setDefaultCommunity() {
// when we already get an identifier via url we choose this if the community exist
if (this.communityIdentifier && this.communities.length >= 1) {
const foundCommunity = this.communities.find((community) => {
if (
community.uuid === this.communityIdentifier ||
community.name === this.communityIdentifier
) {
this.validCommunityIdentifier = true
return true
}
return false
})
if (foundCommunity) {
this.updateCommunity(foundCommunity)
return
}
this.toastError('invalid community identifier in url')
}
if (this.validCommunityIdentifier && !this.communityIdentifier) {
this.validCommunityIdentifier = false
}
// set default community, the only one which isn't foreign
// we assume it is only one entry with foreign = false
if (this.value.uuid === '' && this.communities.length) {
@ -48,12 +77,17 @@ export default {
query: selectCommunities,
},
},
mounted() {
this.setDefaultCommunity()
computed: {
communityIdentifier() {
return this.$route.params && this.$route.params.communityIdentifier
},
},
updated() {
this.setDefaultCommunity()
},
mounted() {
this.setDefaultCommunity()
},
}
</script>
<style>

View File

@ -4,7 +4,7 @@ import flushPromises from 'flush-promises'
import { SEND_TYPES } from '@/pages/Send'
import { createMockClient } from 'mock-apollo-client'
import VueApollo from 'vue-apollo'
import { userAndCommunity, selectCommunities as selectCommunitiesQuery } from '@/graphql/queries'
import { user, selectCommunities as selectCommunitiesQuery } from '@/graphql/queries'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
@ -32,6 +32,9 @@ describe('TransactionForm', () => {
params: {},
query: {},
},
$router: {
replace: jest.fn(),
},
}
const propsData = {
@ -47,23 +50,21 @@ describe('TransactionForm', () => {
})
}
const userAndCommunityMock = jest.fn()
const userMock = jest.fn()
mockClient.setRequestHandler(
userAndCommunity,
userAndCommunityMock
.mockRejectedValueOnce({ message: 'Query user name fails!' })
.mockResolvedValue({
data: {
user: {
firstName: 'Bibi',
lastName: 'Bloxberg',
},
community: {
name: 'Gradido Entwicklung',
},
user,
userMock.mockRejectedValueOnce({ message: 'Query user name fails!' }).mockResolvedValue({
data: {
user: {
firstName: 'Bibi',
lastName: 'Bloxberg',
},
}),
community: {
name: 'Gradido Entwicklung',
},
},
}),
)
mockClient.setRequestHandler(
@ -410,7 +411,8 @@ Die ganze Welt bezwingen.“`)
describe('with gradido ID', () => {
beforeEach(async () => {
jest.clearAllMocks()
mocks.$route.query.gradidoID = 'gradido-ID'
mocks.$route.params.userIdentifier = 'gradido-ID'
mocks.$route.params.communityIdentifier = 'community-ID'
wrapper = Wrapper()
await wrapper.vm.$nextTick()
})
@ -421,8 +423,9 @@ Die ganze Welt bezwingen.“`)
})
it('queries the username', () => {
expect(userAndCommunityMock).toBeCalledWith({
expect(userMock).toBeCalledWith({
identifier: 'gradido-ID',
communityIdentifier: 'community-ID',
})
})
})

View File

@ -55,22 +55,15 @@
</b-row>
<b-row>
<b-col class="font-weight-bold">
<div v-if="!communityUuid">
<community-switch
v-model="form.targetCommunity"
:disabled="isBalanceDisabled"
/>
</div>
<div v-else class="mb-4">
<b-row>
<b-col class="font-weight-bold">{{ recipientCommunity.name }}</b-col>
</b-row>
</div>
<community-switch
v-model="form.targetCommunity"
:disabled="isBalanceDisabled"
/>
</b-col>
</b-row>
</b-col>
<b-col cols="12" v-if="radioSelected === sendTypes.send">
<div v-if="!gradidoID">
<div v-if="!userIdentifier">
<input-identifier
:name="$t('form.recipient')"
:label="$t('form.recipient')"
@ -150,8 +143,7 @@ import InputIdentifier from '@/components/Inputs/InputIdentifier'
import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea'
import CommunitySwitch from '@/components/CommunitySwitch.vue'
import { userAndCommunity } from '@/graphql/queries'
import { isEmpty } from 'lodash'
import { user } from '@/graphql/queries'
import { COMMUNITY_NAME } from '@/config'
export default {
@ -193,11 +185,7 @@ export default {
this.$refs.formValidator.validate()
},
onSubmit() {
if (this.gradidoID) this.form.identifier = this.gradidoID
if (this.communityUuid) {
this.recipientCommunity.uuid = this.communityUuid
this.form.targetCommunity = this.recipientCommunity
}
if (this.userIdentifier) this.form.identifier = this.userIdentifier.identifier
this.$emit('set-transaction', {
selected: this.radioSelected,
identifier: this.form.identifier,
@ -214,29 +202,23 @@ export default {
this.form.memo = ''
this.form.targetCommunity = { uuid: '', name: COMMUNITY_NAME }
this.$refs.formValidator.validate()
if (this.$route.query && !isEmpty(this.$route.query))
this.$router.replace({ query: undefined })
this.$router.replace('/send')
},
},
apollo: {
UserName: {
query() {
return userAndCommunity
return user
},
fetchPolicy: 'network-only',
variables() {
return {
identifier: this.gradidoID,
communityUuid: this.communityUuid,
}
return this.userIdentifier
},
skip() {
return !this.gradidoID
return !this.userIdentifier
},
update({ user, community }) {
update({ user }) {
this.userName = `${user.firstName} ${user.lastName}`
this.recipientCommunity.name = community.name
this.recipientCommunity.uuid = this.communityUuid
},
error({ message }) {
this.toastError(message)
@ -261,11 +243,18 @@ export default {
sendTypes() {
return SEND_TYPES
},
gradidoID() {
return this.$route.query && this.$route.query.gradidoID
},
communityUuid() {
return this.$route.query && this.$route.query.communityUuid
userIdentifier() {
if (
this.$route.params &&
this.$route.params.userIdentifier &&
this.$route.params.communityIdentifier
) {
return {
identifier: this.$route.params.userIdentifier,
communityIdentifier: this.$route.params.communityIdentifier,
}
}
return null
},
},
mounted() {

View File

@ -15,16 +15,20 @@
{{ item.extra }}
<br />
<br />
{{ item.extra2 }}
<a :href="'mailto:' + item.email">{{ item.email }}</a>
<span v-if="item.extra2">
{{ item.extra2 }}
</span>
<span v-if="item.email">
<a :href="'mailto:' + item.email">{{ item.email }}</a>
</span>
</b-col>
</b-row>
<b-row class="my-5">
<b-col cols="12">
<div class="text-lg-right">
<b-button variant="gradido" :to="item.url">
{{ $t('community.startNewsButton') }}
<b-button variant="gradido" :href="item.url" target="_blank">
{{ item.button }}
</b-button>
</div>
</b-col>

View File

@ -47,7 +47,12 @@ describe('Name', () => {
describe('with linked user', () => {
beforeEach(async () => {
await wrapper.setProps({
linkedUser: { firstName: 'Bibi', lastName: 'Bloxberg', gradidoID: 'gradido-ID' },
linkedUser: {
firstName: 'Bibi',
lastName: 'Bloxberg',
gradidoID: 'gradido-ID',
communityUuid: 'community UUID',
},
})
})
@ -70,10 +75,11 @@ describe('Name', () => {
})
})
it('pushes query for gradidoID', () => {
it('pushes params for gradidoID and community UUID', () => {
expect(routerPushMock).toBeCalledWith({
query: {
gradidoID: 'gradido-ID',
params: {
communityIdentifier: 'community UUID',
userIdentifier: 'gradido-ID',
},
})
})

View File

@ -37,9 +37,9 @@ export default {
async tunnelEmail() {
if (this.$route.path !== '/send') await this.$router.push({ path: '/send' })
this.$router.push({
query: {
gradidoID: this.linkedUser.gradidoID,
communityUuid: this.linkedUser.communityUuid,
params: {
userIdentifier: this.linkedUser.gradidoID,
communityIdentifier: this.linkedUser.communityUuid,
},
})
},

View File

@ -285,23 +285,10 @@ export const openCreations = gql`
`
export const user = gql`
query($identifier: String!) {
user(identifier: $identifier) {
firstName
lastName
communityName
}
}
`
export const userAndCommunity = gql`
query($identifier: String!, $communityUuid: String!) {
user(identifier: $identifier) {
query($identifier: String!, $communityIdentifier: String!) {
user(identifier: $identifier, communityIdentifier: $communityIdentifier) {
firstName
lastName
}
community(communityUuid: $communityUuid) {
name
}
}
`

View File

@ -31,7 +31,6 @@
"moderator": "Moderator",
"moderators": "Moderatoren",
"myContributions": "Meine Beiträge",
"startNewsButton": "Gradidos versenden",
"submitContribution": "Schreiben"
},
"communityInfo": "Gemeinschaft Information",

View File

@ -31,7 +31,6 @@
"moderator": "Moderator",
"moderators": "Moderators",
"myContributions": "My contributions",
"startNewsButton": "Send Gradidos",
"submitContribution": "Contribute"
},
"communityInfo": "Community Information",

View File

@ -29,6 +29,7 @@ describe('Login', () => {
commit: mockStoreCommit,
state: {
publisherId: 12345,
redirectPath: '/overview',
},
},
$loading: {

View File

@ -106,7 +106,7 @@ export default {
if (this.$route.params.code) {
this.$router.push(`/redeem/${this.$route.params.code}`)
} else {
this.$router.push('/overview')
this.$router.push(this.$store.state.redirectPath)
}
})
.catch((error) => {

View File

@ -38,6 +38,7 @@ describe('Send', () => {
},
$route: {
query: {},
params: {},
},
$router: {
push: routerPushMock,
@ -175,7 +176,9 @@ describe('Send', () => {
describe('with gradidoID query', () => {
beforeEach(() => {
mocks.$route.query.gradidoID = 'gradido-ID'
jest.clearAllMocks()
mocks.$route.params.userIdentifier = 'gradido-ID'
mocks.$route.params.communityIdentifier = 'community-ID'
wrapper = Wrapper()
})
@ -226,11 +229,7 @@ describe('Send', () => {
})
it('resets the gradido ID query in route', () => {
expect(routerPushMock).toBeCalledWith({
query: {
gradidoID: undefined,
},
})
expect(routerPushMock).toBeCalledWith('send')
})
})
})

View File

@ -172,7 +172,7 @@ export default {
throw new Error(`undefined transactionData.selected : ${this.transactionData.selected}`)
}
this.loading = false
this.$router.push({ query: { gradidoID: undefined, communityUuid: undefined } })
this.$router.push('send')
},
onBack() {
this.currentTransactionStep = TRANSACTION_STEPS.transactionForm

View File

@ -36,6 +36,8 @@ const addNavigationGuards = (router, store, apollo) => {
// handle authentication
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.token) {
// store redirect path
store.commit('redirectPath', to.path)
next({ path: '/login' })
} else {
next()

View File

@ -66,11 +66,11 @@ describe('router', () => {
describe('send', () => {
it('requires authorization', () => {
expect(routes.find((r) => r.path === '/send').meta.requiresAuth).toBeTruthy()
expect(routes.find((r) => r.path.startsWith('/send')).meta.requiresAuth).toBeTruthy()
})
it('loads the "Send" page', async () => {
const component = await routes.find((r) => r.path === '/send').component()
const component = await routes.find((r) => r.path.startsWith('/send')).component()
expect(component.default.name).toBe('Send')
})
})

View File

@ -19,7 +19,9 @@ const routes = [
},
},
{
path: '/send',
// userIdentifier can be username, email or gradidoID
// communityIdentifier can be community name or community UUID
path: '/send/:communityIdentifier?/:userIdentifier?',
component: () => import('@/pages/Send'),
meta: {
requiresAuth: true,

View File

@ -58,6 +58,9 @@ export const mutations = {
setDarkMode: (state, darkMode) => {
state.darkMode = !!darkMode
},
redirectPath: (state, redirectPath) => {
state.redirectPath = redirectPath || '/overview'
},
}
export const actions = {
@ -89,6 +92,7 @@ export const actions = {
commit('hideAmountGDT', true)
commit('email', '')
commit('setDarkMode', false)
commit('redirectPath', '/overview')
localStorage.clear()
},
}
@ -119,6 +123,7 @@ try {
hideAmountGDT: null,
email: '',
darkMode: false,
redirectPath: '/overview',
},
getters: {},
// Syncronous mutation of the state

View File

@ -264,7 +264,7 @@ describe('Vuex store', () => {
it('calls twelve commits', () => {
logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(13)
expect(commit).toHaveBeenCalledTimes(14)
})
it('commits token', () => {