From 0dd70db539b5bb800aff93f48cded0c90bb7d7be Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Wed, 24 Jan 2024 18:15:33 +0100 Subject: [PATCH 1/9] fix not working fail2ban, add jails for nginx --- deployment/bare_metal/nginx/common/limit_requests.conf | 3 ++- deployment/hetzner_cloud/cloudConfig.yaml | 1 + deployment/hetzner_cloud/install.sh | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/deployment/bare_metal/nginx/common/limit_requests.conf b/deployment/bare_metal/nginx/common/limit_requests.conf index e9026ee81..c9501fd64 100644 --- a/deployment/bare_metal/nginx/common/limit_requests.conf +++ b/deployment/bare_metal/nginx/common/limit_requests.conf @@ -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; \ No newline at end of file +limit_req_zone $binary_remote_addr zone=api:5m rate=30r/s; +limit_conn_zone $binary_remote_addr zone=addr:10m; \ No newline at end of file diff --git a/deployment/hetzner_cloud/cloudConfig.yaml b/deployment/hetzner_cloud/cloudConfig.yaml index 86e7d5724..84658705f 100644 --- a/deployment/hetzner_cloud/cloudConfig.yaml +++ b/deployment/hetzner_cloud/cloudConfig.yaml @@ -9,6 +9,7 @@ users: packages: - fail2ban + - python3-systemd - ufw - git - mariadb-server diff --git a/deployment/hetzner_cloud/install.sh b/deployment/hetzner_cloud/install.sh index ee539370c..e9ed69e76 100755 --- a/deployment/hetzner_cloud/install.sh +++ b/deployment/hetzner_cloud/install.sh @@ -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 From 491c09f835c8cd700b97c9556adfba060128c6c5 Mon Sep 17 00:00:00 2001 From: einhorn_b Date: Wed, 24 Jan 2024 18:28:28 +0100 Subject: [PATCH 2/9] move definition of zones --- .../bare_metal/nginx/sites-available/gradido.conf.ssl.template | 3 ++- .../bare_metal/nginx/sites-available/gradido.conf.template | 3 ++- .../nginx/sites-available/update-page.conf.ssl.template | 2 +- .../bare_metal/nginx/sites-available/update-page.conf.template | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template index 822c326d0..d8ed50ba4 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template @@ -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; diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.template index 1f673ee41..e0f382467 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.template @@ -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; diff --git a/deployment/bare_metal/nginx/sites-available/update-page.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/update-page.conf.ssl.template index ee7732230..fd41c333d 100644 --- a/deployment/bare_metal/nginx/sites-available/update-page.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/update-page.conf.ssl.template @@ -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; diff --git a/deployment/bare_metal/nginx/sites-available/update-page.conf.template b/deployment/bare_metal/nginx/sites-available/update-page.conf.template index 38dfb2d02..be91abc88 100644 --- a/deployment/bare_metal/nginx/sites-available/update-page.conf.template +++ b/deployment/bare_metal/nginx/sites-available/update-page.conf.template @@ -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; From ee40603637d55921bb000fbe2e81cbf9cdb4fbea Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 30 Jan 2024 21:39:12 +0100 Subject: [PATCH 3/9] use params instead of query for send/identifier route --- backend/src/config/index.ts | 2 +- backend/src/graphql/arg/UserArgs.ts | 6 +- backend/src/graphql/model/User.ts | 3 + .../graphql/resolver/TransactionResolver.ts | 2 +- backend/src/graphql/resolver/UserResolver.ts | 7 +- .../resolver/util/findUserByIdentifier.ts | 39 +++-- backend/src/util/communityUser.ts | 1 + .../0081-user_join_community/Community.ts | 70 +++++++++ .../entity/0081-user_join_community/User.ts | 138 ++++++++++++++++++ database/entity/Community.ts | 2 +- database/entity/User.ts | 2 +- .../migrations/0081-user_join_community.ts | 11 ++ dht-node/src/config/index.ts | 2 +- federation/src/config/index.ts | 2 +- frontend/src/components/CommunitySwitch.vue | 54 +++++-- .../components/GddSend/TransactionForm.vue | 58 +++----- frontend/src/graphql/queries.js | 17 +-- frontend/src/pages/Send.vue | 2 +- frontend/src/routes/routes.js | 4 +- 19 files changed, 328 insertions(+), 94 deletions(-) create mode 100644 database/entity/0081-user_join_community/Community.ts create mode 100644 database/entity/0081-user_join_community/User.ts create mode 100644 database/migrations/0081-user_join_community.ts diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 1ec5a98e6..ee90261f4 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,7 +12,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0080-fill_linked_user_gradidoId_of_contributions', + DB_VERSION: '0081-user_join_community', 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 diff --git a/backend/src/graphql/arg/UserArgs.ts b/backend/src/graphql/arg/UserArgs.ts index 633ed5e20..406be14cb 100644 --- a/backend/src/graphql/arg/UserArgs.ts +++ b/backend/src/graphql/arg/UserArgs.ts @@ -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 } diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index cd188f49f..d24a717c4 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -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) { diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index b4fd5c4e3..00894ecd3 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -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, diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 1f21abbb9..6079ac634 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -428,7 +428,7 @@ export class UserResolver { const userContact = await DbUserContact.findOneOrFail({ where: { emailVerificationCode: code }, relations: ['user'], - }).catch(() => { + }).catch((e) => { throw new LogError('Could not login with emailVerificationCode') }) logger.debug('userContact loaded...') @@ -821,11 +821,6 @@ export class UserResolver { ): Promise { 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 } } diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts index 6b7f1bccc..633049afb 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifier.ts @@ -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 => { let user: DbUser | null + const communityWhere: FindOptionsWhere = + 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,24 @@ export const findUserByIdentifier = async ( where: { email: identifier, emailChecked: true, + user: { + community: communityWhere, + }, }, - relations: ['user'], + relations: ['user', 'user.community'], }) if (!userContact) { - throw new LogError('No user with this credentials', identifier) + throw new LogError('No user with this credentials', identifier, communityIdentifier) } 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 to given contact', 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) diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index bad06f201..c4c420d51 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -50,6 +50,7 @@ const communityDbUser: dbUser = { }, foreign: false, communityUuid: '55555555-4444-4333-2222-11111111', + community: null, } const communityUser = new User(communityDbUser) diff --git a/database/entity/0081-user_join_community/Community.ts b/database/entity/0081-user_join_community/Community.ts new file mode 100644 index 000000000..1c6b36be3 --- /dev/null +++ b/database/entity/0081-user_join_community/Community.ts @@ -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[] +} diff --git a/database/entity/0081-user_join_community/User.ts b/database/entity/0081-user_join_community/User.ts new file mode 100644 index 000000000..28141029d --- /dev/null +++ b/database/entity/0081-user_join_community/User.ts @@ -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[] +} diff --git a/database/entity/Community.ts b/database/entity/Community.ts index d398cf584..d286749eb 100644 --- a/database/entity/Community.ts +++ b/database/entity/Community.ts @@ -1 +1 @@ -export { Community } from './0068-community_tables_public_key_length/Community' +export { Community } from './0081-user_join_community/Community' diff --git a/database/entity/User.ts b/database/entity/User.ts index 21785ee9c..b75693674 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0073-introduce_foreign_user_in_users_table/User' +export { User } from './0081-user_join_community/User' diff --git a/database/migrations/0081-user_join_community.ts b/database/migrations/0081-user_join_community.ts new file mode 100644 index 000000000..6e40cb414 --- /dev/null +++ b/database/migrations/0081-user_join_community.ts @@ -0,0 +1,11 @@ +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + 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>) { + await queryFn( + 'ALTER TABLE users MODIFY community_uuid VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;', + ) +} diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index fd3df5c21..632ccdba3 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -4,7 +4,7 @@ import dotenv from 'dotenv' dotenv.config() const constants = { - DB_VERSION: '0080-fill_linked_user_gradidoId_of_contributions', + DB_VERSION: '0081-user_join_community', LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 821df574a..8a8947b93 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0080-fill_linked_user_gradidoId_of_contributions', + DB_VERSION: '0081-user_join_community', 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 diff --git a/frontend/src/components/CommunitySwitch.vue b/frontend/src/components/CommunitySwitch.vue index f9ecc804d..2c47dd08e 100644 --- a/frontend/src/components/CommunitySwitch.vue +++ b/frontend/src/components/CommunitySwitch.vue @@ -1,16 +1,23 @@