Merge pull request #3311 from gradido/humhub_login_link

feat(frontend): auto-login link for humhub
This commit is contained in:
einhornimmond 2024-05-10 15:19:17 +02:00 committed by GitHub
commit 0c1b88e9ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 492 additions and 426 deletions

View File

@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 82,
lines: 81,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -1,8 +1,8 @@
import { User as dbUser } from '@entity/User'
import { GmsPublishLocationType } from '@/graphql/enum/GmsPublishLocationType'
import { GmsPublishNameType } from '@/graphql/enum/GmsPublishNameType'
import { GmsPublishPhoneType } from '@/graphql/enum/GmsPublishPhoneType'
import { PublishNameType } from '@/graphql/enum/PublishNameType'
export class GmsUser {
constructor(user: dbUser) {
@ -44,7 +44,7 @@ export class GmsUser {
if (
user.gmsAllowed &&
user.alias &&
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS
user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS
) {
return user.alias
}
@ -53,32 +53,30 @@ export class GmsUser {
private getGmsFirstName(user: dbUser): string | undefined {
if (
user.gmsAllowed &&
(user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FIRST ||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL ||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FULL)
(user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST ||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST_INITIAL ||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_FULL)
) {
return user.firstName
}
if (
user.gmsAllowed &&
((!user.alias &&
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS) ||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_INITIALS)
((!user.alias && user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_INITIALS)
) {
return user.firstName.substring(0, 1)
}
}
private getGmsLastName(user: dbUser): string | undefined {
if (user.gmsAllowed && user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FULL) {
if (user.gmsAllowed && user.gmsPublishName === PublishNameType.PUBLISH_NAME_FULL) {
return user.lastName
}
if (
user.gmsAllowed &&
((!user.alias &&
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS) ||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL ||
user.gmsPublishName === GmsPublishNameType.GMS_PUBLISH_NAME_INITIALS)
((!user.alias && user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST_INITIAL ||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_INITIALS)
) {
return user.lastName.substring(0, 1)
}

View File

@ -1,10 +1,11 @@
import { SignJWT } from 'jose'
import { IRequestOptions, RestClient } from 'typed-rest-client'
import { IRequestOptions, IRestResponse, RestClient } from 'typed-rest-client'
import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { PostUserLoggingView } from './logging/PostUserLogging.view'
import { GetUser } from './model/GetUser'
import { PostUser } from './model/PostUser'
import { UsersResponse } from './model/UsersResponse'
@ -58,6 +59,18 @@ export class HumHubClient {
return token
}
public async createAutoLoginUrl(username: string) {
const secret = new TextEncoder().encode(CONFIG.HUMHUB_JWT_KEY)
logger.info(`user ${username} as username for humhub auto-login`)
const token = await new SignJWT({ username })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('2m')
.sign(secret)
return `${CONFIG.HUMHUB_API_URL}user/auth/external?authclient=jwt&jwt=${token}`
}
/**
* Get all users from humhub
* https://marketplace.humhub.com/module/rest/docs/html/user.html#tag/User/paths/~1user/get
@ -90,12 +103,38 @@ export class HumHubClient {
return response.result
}
public async userByEmailAsync(email: string): Promise<IRestResponse<GetUser>> {
const options = await this.createRequestOptions({ email })
return this.restClient.get<GetUser>('/api/v1/user/get-by-email', options)
}
public async userByUsernameAsync(username: string): Promise<IRestResponse<GetUser>> {
const options = await this.createRequestOptions({ username })
return this.restClient.get<GetUser>('/api/v1/user/get-by-username', options)
}
/**
* get user by username
* https://marketplace.humhub.com/module/rest/docs/html/user.html#tag/User/paths/~1user~1get-by-username/get
* @param username for user search
* @returns user object if found
*/
public async userByUsername(username: string): Promise<GetUser | null> {
const options = await this.createRequestOptions({ username })
const response = await this.restClient.get<GetUser>('/api/v1/user/get-by-username', options)
if (response.statusCode === 404) {
return null
}
return response.result
}
/**
* create user
* https://marketplace.humhub.com/module/rest/docs/html/user.html#tag/User/paths/~1user/post
* @param user for saving on humhub instance
*/
public async createUser(user: PostUser): Promise<void> {
logger.info('create new humhub user', new PostUserLoggingView(user))
const options = await this.createRequestOptions()
try {
const response = await this.restClient.create('/api/v1/user', user, options)
@ -118,6 +157,7 @@ export class HumHubClient {
* @returns updated user object on success
*/
public async updateUser(user: PostUser, humhubUserId: number): Promise<GetUser | null> {
logger.info('update humhub user', new PostUserLoggingView(user))
const options = await this.createRequestOptions()
const response = await this.restClient.update<GetUser>(
`/api/v1/user/${humhubUserId}`,
@ -133,6 +173,7 @@ export class HumHubClient {
}
public async deleteUser(humhubUserId: number): Promise<void> {
logger.info('delete humhub user', { userId: humhubUserId })
const options = await this.createRequestOptions()
const response = await this.restClient.del(`/api/v1/user/${humhubUserId}`, options)
if (response.statusCode === 400) {

View File

@ -1,5 +1,6 @@
import { User } from '@entity/User'
import { UserContact } from '@entity/UserContact'
import { IRestResponse } from 'typed-rest-client'
import { GetUser } from '@/apis/humhub/model/GetUser'
import { PostUser } from '@/apis/humhub/model/PostUser'
@ -33,6 +34,25 @@ export class HumHubClient {
return Promise.resolve(new GetUser(user, 1))
}
public async userByEmailAsync(email: string): Promise<IRestResponse<GetUser>> {
const user = new User()
user.emailContact = new UserContact()
user.emailContact.email = email
return Promise.resolve({
statusCode: 200,
result: new GetUser(user, 1),
headers: {},
})
}
public async userByUsername(username: string): Promise<GetUser | null> {
const user = new User()
user.alias = username
user.emailContact = new UserContact()
user.emailContact.email = 'testemail@gmail.com'
return Promise.resolve(new GetUser(user, 1))
}
public async createUser(): Promise<void> {
return Promise.resolve()
}

View File

@ -17,6 +17,7 @@ function accountIsTheSame(account: Account, user: User): boolean {
if (account.username !== gradidoUserAccount.username) return false
if (account.email !== gradidoUserAccount.email) return false
if (account.language !== gradidoUserAccount.language) return false
if (account.status !== gradidoUserAccount.status) return false
return true
}

View File

@ -0,0 +1,18 @@
import { AbstractLoggingView } from '@logging/AbstractLogging.view'
import { Account } from '@/apis/humhub/model/Account'
export class AccountLoggingView extends AbstractLoggingView {
public constructor(private self: Account) {
super()
}
public toJSON(): Account {
return {
username: this.self.username.substring(0, 3) + '...',
email: this.self.email.substring(0, 3) + '...',
language: this.self.language,
status: this.self.status,
}
}
}

View File

@ -0,0 +1,23 @@
import { AbstractLoggingView } from '@logging/AbstractLogging.view'
import { PostUser } from '@/apis/humhub/model/PostUser'
import { AccountLoggingView } from './AccountLogging.view'
import { ProfileLoggingView } from './ProfileLogging.view'
export class PostUserLoggingView extends AbstractLoggingView {
public constructor(private self: PostUser) {
super()
}
public toJSON(): PostUser {
return {
account: new AccountLoggingView(this.self.account).toJSON(),
profile: new ProfileLoggingView(this.self.profile).toJSON(),
password: {
newPassword: '',
mustChangePassword: false,
},
}
}
}

View File

@ -0,0 +1,20 @@
import { AbstractLoggingView } from '@logging/AbstractLogging.view'
import { Profile } from '@/apis/humhub/model/Profile'
export class ProfileLoggingView extends AbstractLoggingView {
public constructor(private self: Profile) {
super()
}
public toJSON(): Profile {
const gradidoAddressParts = this.self.gradido_address.split('/')
return {
firstname: this.self.firstname.substring(0, 3) + '...',
lastname: this.self.lastname.substring(0, 3) + '...',
// eslint-disable-next-line camelcase
gradido_address:
gradidoAddressParts[0] + '/' + gradidoAddressParts[1].substring(0, 3) + '...',
}
}
}

View File

@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import { User } from '@entity/User'
import { convertGradidoLanguageToHumhub } from '@/apis/humhub/convertLanguage'
@ -12,9 +13,11 @@ export class Account {
this.email = user.emailContact.email
this.language = convertGradidoLanguageToHumhub(user.language)
this.status = 1
}
username: string
email: string
language: string
status: number
}

View File

@ -38,6 +38,7 @@ export enum RIGHTS {
OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER',
GMS_USER_PLAYGROUND = 'GMS_USER_PLAYGROUND',
HUMHUB_AUTO_LOGIN = 'HUMHUB_AUTO_LOGIN',
// Moderator
SEARCH_USERS = 'SEARCH_USERS',
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',

View File

@ -30,4 +30,5 @@ export const USER_RIGHTS = [
RIGHTS.OPEN_CREATIONS,
RIGHTS.USER,
RIGHTS.GMS_USER_PLAYGROUND,
RIGHTS.HUMHUB_AUTO_LOGIN,
]

View File

@ -21,13 +21,16 @@ export class PublishNameLogic {
) {
return this.user.firstName
}
if (
[PublishNameType.PUBLISH_NAME_INITIALS, PublishNameType.PUBLISH_NAME_INITIAL_LAST].includes(
publishNameType,
)
) {
if (PublishNameType.PUBLISH_NAME_INITIALS === publishNameType) {
return this.user.firstName.substring(0, 1)
}
if (PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS === publishNameType) {
if (this.user.alias) {
return this.user.alias
} else {
return this.user.firstName.substring(0, 1)
}
}
return ''
}
@ -38,22 +41,21 @@ export class PublishNameLogic {
* first initial from user.lastName for PUBLISH_NAME_FIRST_INITIAL, PUBLISH_NAME_INITIALS
*/
public getLastName(publishNameType: PublishNameType): string {
if (
[
PublishNameType.PUBLISH_NAME_LAST,
PublishNameType.PUBLISH_NAME_INITIAL_LAST,
PublishNameType.PUBLISH_NAME_FULL,
].includes(publishNameType)
) {
if (PublishNameType.PUBLISH_NAME_FULL === publishNameType) {
return this.user.lastName
}
if (
} else if (
[PublishNameType.PUBLISH_NAME_FIRST_INITIAL, PublishNameType.PUBLISH_NAME_INITIALS].includes(
publishNameType,
)
) {
return this.user.lastName.substring(0, 1)
} else if (
PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS === publishNameType &&
!this.user.alias
) {
return this.user.lastName.substring(0, 1)
}
return ''
}
}

View File

@ -2,7 +2,6 @@ import { IsBoolean, IsEnum, IsInt, IsString } from 'class-validator'
import { ArgsType, Field, InputType, Int } from 'type-graphql'
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
import { PublishNameType } from '@enum/PublishNameType'
import { Location } from '@model/Location'
@ -55,9 +54,9 @@ export class UpdateUserInfosArgs {
@IsBoolean()
gmsAllowed?: boolean
@Field(() => GmsPublishNameType, { nullable: true })
@IsEnum(GmsPublishNameType)
gmsPublishName?: GmsPublishNameType | null
@Field(() => PublishNameType, { nullable: true })
@IsEnum(PublishNameType)
gmsPublishName?: PublishNameType | null
@Field(() => PublishNameType, { nullable: true })
@IsEnum(PublishNameType)

View File

@ -1,14 +0,0 @@
import { registerEnumType } from 'type-graphql'
export enum GmsPublishNameType {
GMS_PUBLISH_NAME_ALIAS_OR_INITALS = 0,
GMS_PUBLISH_NAME_INITIALS = 1,
GMS_PUBLISH_NAME_FIRST = 2,
GMS_PUBLISH_NAME_FIRST_INITIAL = 3,
GMS_PUBLISH_NAME_FULL = 4,
}
registerEnumType(GmsPublishNameType, {
name: 'GmsPublishNameType', // this one is mandatory
description: 'Type of name publishing', // this one is optional
})

View File

@ -1,19 +1,14 @@
import { registerEnumType } from 'type-graphql'
/**
* Enum for decide which parts from first- and last-name are allowed to be published in an extern service
*/
export enum PublishNameType {
PUBLISH_NAME_NONE = 0,
PUBLISH_NAME_ALIAS_OR_INITALS = 0,
PUBLISH_NAME_INITIALS = 1,
PUBLISH_NAME_FIRST = 2,
PUBLISH_NAME_FIRST_INITIAL = 3,
PUBLISH_NAME_LAST = 4,
PUBLISH_NAME_INITIAL_LAST = 5,
PUBLISH_NAME_FULL = 6,
PUBLISH_NAME_FULL = 4,
}
registerEnumType(PublishNameType, {
name: 'PublishNameType', // this one is mandatory
description: 'Type of first- and last-name publishing for extern service', // this one is optional
description: 'Type of name publishing', // this one is optional
})

View File

@ -2,7 +2,6 @@ import { User as dbUser } from '@entity/User'
import { ObjectType, Field, Int } from 'type-graphql'
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
import { PublishNameType } from '@enum/PublishNameType'
import { KlickTipp } from './KlickTipp'
@ -89,8 +88,8 @@ export class User {
@Field(() => Boolean)
gmsAllowed: boolean
@Field(() => GmsPublishNameType, { nullable: true })
gmsPublishName: GmsPublishNameType | null
@Field(() => PublishNameType, { nullable: true })
gmsPublishName: PublishNameType | null
@Field(() => PublishNameType, { nullable: true })
humhubPublishName: PublishNameType | null

View File

@ -17,7 +17,6 @@ import { GraphQLError } from 'graphql'
import { v4 as uuidv4, validate as validateUUID, version as versionUUID } from 'uuid'
import { GmsPublishLocationType } from '@enum/GmsPublishLocationType'
import { GmsPublishNameType } from '@enum/GmsPublishNameType'
import { OptInType } from '@enum/OptInType'
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
import { RoleNames } from '@enum/RoleNames'
@ -35,6 +34,7 @@ import {
sendResetPasswordEmail,
} from '@/emails/sendEmailVariants'
import { EventType } from '@/event/Events'
import { PublishNameType } from '@/graphql/enum/PublishNameType'
import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
import { encryptPassword } from '@/password/PasswordEncryptor'
import { writeHomeCommunityEntry } from '@/seeds/community'
@ -73,6 +73,8 @@ import { objectValuesToArray } from '@/util/utilities'
import { Location2Point } from './util/Location2Point'
jest.mock('@/apis/humhub/HumHubClient')
jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
return {
@ -1232,7 +1234,7 @@ describe('UserResolver', () => {
lastName: 'Blümchen',
language: 'en',
gmsAllowed: true,
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
gmsPublishName: PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS,
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
}),
])
@ -1272,7 +1274,7 @@ describe('UserResolver', () => {
expect.objectContaining({
alias: 'bibi_Bloxberg',
gmsAllowed: true,
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
gmsPublishName: PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS,
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
}),
])
@ -1294,7 +1296,7 @@ describe('UserResolver', () => {
await expect(User.find()).resolves.toEqual([
expect.objectContaining({
gmsAllowed: true,
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
gmsPublishName: PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS,
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
}),
])
@ -1307,8 +1309,7 @@ describe('UserResolver', () => {
mutation: updateUserInfos,
variables: {
gmsAllowed: false,
gmsPublishName:
GmsPublishNameType[GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL],
gmsPublishName: PublishNameType[PublishNameType.PUBLISH_NAME_FIRST_INITIAL],
gmsPublishLocation:
GmsPublishLocationType[GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE],
},
@ -1316,7 +1317,7 @@ describe('UserResolver', () => {
await expect(User.find()).resolves.toEqual([
expect.objectContaining({
gmsAllowed: false,
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL,
gmsPublishName: PublishNameType.PUBLISH_NAME_FIRST_INITIAL,
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE,
}),
])
@ -1332,8 +1333,7 @@ describe('UserResolver', () => {
mutation: updateUserInfos,
variables: {
gmsAllowed: true,
gmsPublishName:
GmsPublishNameType[GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS],
gmsPublishName: PublishNameType[PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS],
gmsLocation: loc,
gmsPublishLocation:
GmsPublishLocationType[GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM],
@ -1342,7 +1342,7 @@ describe('UserResolver', () => {
await expect(User.find()).resolves.toEqual([
expect.objectContaining({
gmsAllowed: true,
gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS,
gmsPublishName: PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS,
location: Location2Point(loc),
gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM,
}),

View File

@ -10,6 +10,7 @@ import { UserContact as DbUserContact } from '@entity/UserContact'
import { UserRole } from '@entity/UserRole'
import i18n from 'i18n'
import { Resolver, Query, Args, Arg, Authorized, Ctx, Mutation, Int } from 'type-graphql'
import { IRestResponse } from 'typed-rest-client'
import { v4 as uuidv4 } from 'uuid'
import { UserArgs } from '@arg//UserArgs'
@ -31,6 +32,8 @@ import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
import { updateGmsUser } from '@/apis/gms/GmsClient'
import { GmsUser } from '@/apis/gms/model/GmsUser'
import { HumHubClient } from '@/apis/humhub/HumHubClient'
import { GetUser } from '@/apis/humhub/model/GetUser'
import { subscribe } from '@/apis/KlicktippController'
import { encode } from '@/auth/JWT'
import { RIGHTS } from '@/auth/RIGHTS'
@ -157,11 +160,17 @@ export class UserResolver {
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new LogError('The User has not set a password yet', dbUser)
}
if (!verifyPassword(dbUser, password)) {
throw new LogError('No user with this credentials', dbUser)
}
// request to humhub and klicktipp run in parallel
let humhubUserPromise: Promise<IRestResponse<GetUser>> | undefined
const klicktippStatePromise = getKlicktippState(dbUser.emailContact.email)
if (CONFIG.HUMHUB_ACTIVE && dbUser.humhubAllowed) {
humhubUserPromise = HumHubClient.getInstance()?.userByUsernameAsync(email)
}
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
dbUser.password = encryptPassword(dbUser, password)
@ -183,7 +192,6 @@ export class UserResolver {
dbUser.publisherId = publisherId
await DbUser.save(dbUser)
}
user.klickTipp = await getKlicktippState(dbUser.emailContact.email)
context.setHeaders.push({
key: 'token',
@ -191,6 +199,12 @@ export class UserResolver {
})
await EVENT_USER_LOGIN(dbUser)
// load humhub state
if (humhubUserPromise) {
const result = await humhubUserPromise
user.humhubAllowed = result?.result?.account.status === 1
}
user.klickTipp = await klicktippStatePromise
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
return user
}
@ -703,6 +717,26 @@ export class UserResolver {
return result
}
@Authorized([RIGHTS.HUMHUB_AUTO_LOGIN])
@Query(() => String)
async authenticateHumhubAutoLogin(@Ctx() context: Context): Promise<string> {
logger.info(`authenticateHumhubAutoLogin()...`)
const dbUser = getUser(context)
const humhubClient = HumHubClient.getInstance()
if (!humhubClient) {
throw new LogError('cannot create humhub client')
}
const username = dbUser.alias ?? dbUser.gradidoID
const humhubUser = await humhubClient.userByUsername(username)
if (!humhubUser) {
throw new LogError("user don't exist (any longer) on humhub")
}
if (humhubUser.account.status !== 1) {
throw new LogError('user status is not 1', humhubUser.account.status)
}
return await humhubClient.createAutoLoginUrl(username)
}
@Authorized([RIGHTS.SEARCH_ADMIN_USERS])
@Query(() => SearchAdminUsersResult)
async searchAdminUsers(

View File

@ -2,7 +2,7 @@ import { Point } from '@dbTools/typeorm'
import { User as DbUser } from '@entity/User'
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
import { GmsPublishNameType } from '@/graphql/enum/GmsPublishNameType'
import { PublishNameType } from '@/graphql/enum/PublishNameType'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
@ -22,11 +22,10 @@ export function compareGmsRelevantUserSettings(
orgUser.alias !== updateUserInfosArgs.alias &&
((updateUserInfosArgs.gmsPublishName &&
updateUserInfosArgs.gmsPublishName.valueOf ===
GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS.valueOf) ||
PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS.valueOf) ||
(!updateUserInfosArgs.gmsPublishName &&
orgUser.gmsPublishName &&
orgUser.gmsPublishName.valueOf ===
GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS.valueOf))
orgUser.gmsPublishName.valueOf === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS.valueOf))
) {
return true
}

View File

@ -3,7 +3,6 @@ import { UserContact } from '@entity/UserContact'
import { HumHubClient } from '@/apis/humhub/HumHubClient'
import { GetUser } from '@/apis/humhub/model/GetUser'
import { ExecutedHumhubAction } from '@/apis/humhub/syncUser'
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
import { PublishNameType } from '@/graphql/enum/PublishNameType'
import { backendLogger as logger } from '@/server/logger'
@ -22,19 +21,12 @@ const mockUpdateUserInfosArg = new UpdateUserInfosArgs()
const mockHumHubUser = new GetUser(mockUser, 1)
describe('syncHumhub', () => {
beforeAll(() => {
// humhubClientMockbBeforeAll()
})
beforeEach(() => {
jest.spyOn(logger, 'debug').mockImplementation()
jest.spyOn(logger, 'info').mockImplementation()
jest.spyOn(HumHubClient, 'getInstance')
})
afterEach(() => {
// humhubClientMockbAfterEach()
})
afterAll(() => {
jest.resetAllMocks()
})
@ -43,7 +35,7 @@ describe('syncHumhub', () => {
await syncHumhub(mockUpdateUserInfosArg, new User())
expect(HumHubClient.getInstance).not.toBeCalled()
// language logging from some other place
expect(logger.debug).toBeCalledTimes(4)
expect(logger.debug).toBeCalledTimes(5)
expect(logger.info).toBeCalledTimes(0)
})
@ -51,11 +43,11 @@ describe('syncHumhub', () => {
mockUpdateUserInfosArg.firstName = 'New' // Relevant changes
mockUser.firstName = 'New'
await syncHumhub(mockUpdateUserInfosArg, mockUser)
expect(logger.debug).toHaveBeenCalledTimes(7) // Four language logging calls, two debug calls in function, one for not syncing
expect(logger.debug).toHaveBeenCalledTimes(8) // Four language logging calls, two debug calls in function, one for not syncing
expect(logger.info).toHaveBeenLastCalledWith('finished sync user with humhub', {
localId: mockUser.id,
externId: mockHumHubUser.id,
result: ExecutedHumhubAction.UPDATE,
result: 'UPDATE',
})
})

View File

@ -2,7 +2,7 @@ import { User } from '@entity/User'
import { HumHubClient } from '@/apis/humhub/HumHubClient'
import { GetUser } from '@/apis/humhub/model/GetUser'
import { syncUser } from '@/apis/humhub/syncUser'
import { ExecutedHumhubAction, syncUser } from '@/apis/humhub/syncUser'
import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
import { backendLogger as logger } from '@/server/logger'
@ -12,13 +12,14 @@ export async function syncHumhub(
): Promise<void> {
// check for humhub relevant changes
if (
!updateUserInfosArg.alias &&
!updateUserInfosArg.firstName &&
!updateUserInfosArg.lastName &&
!updateUserInfosArg.humhubAllowed &&
!updateUserInfosArg.humhubPublishName &&
!updateUserInfosArg.language
updateUserInfosArg.alias === undefined &&
updateUserInfosArg.firstName === undefined &&
updateUserInfosArg.lastName === undefined &&
updateUserInfosArg.humhubAllowed === undefined &&
updateUserInfosArg.humhubPublishName === undefined &&
updateUserInfosArg.language === undefined
) {
logger.debug('no relevant changes')
return
}
logger.debug('changed user-settings relevant for humhub-user update...')
@ -27,7 +28,7 @@ export async function syncHumhub(
return
}
logger.debug('retrieve user from humhub')
const humhubUser = await humhubClient.userByEmail(user.emailContact.email)
const humhubUser = await humhubClient.userByUsername(user.alias ?? user.gradidoID)
const humhubUsers = new Map<string, GetUser>()
if (humhubUser) {
humhubUsers.set(user.emailContact.email, humhubUser)
@ -37,6 +38,9 @@ export async function syncHumhub(
logger.info('finished sync user with humhub', {
localId: user.id,
externId: humhubUser?.id,
result,
// for preventing this warning https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-object-injection.md
// and possible danger coming with it
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
result: ExecutedHumhubAction[result as ExecutedHumhubAction],
})
}

View File

@ -35,7 +35,7 @@ export const updateUserInfos = gql`
$hideAmountGDD: Boolean
$hideAmountGDT: Boolean
$gmsAllowed: Boolean
$gmsPublishName: GmsPublishNameType
$gmsPublishName: PublishNameType
$gmsLocation: Location
$gmsPublishLocation: GmsPublishLocationType
) {

View File

@ -25,7 +25,7 @@ EMAIL_CODE_REQUEST_TIME=10
# Need to adjust by updates
# config versions
DATABASE_CONFIG_VERSION=v1.2022-03-18
BACKEND_CONFIG_VERSION=v21.2024-01-06
BACKEND_CONFIG_VERSION=v23.2024-04-04
FRONTEND_CONFIG_VERSION=v6.2024-02-27
ADMIN_CONFIG_VERSION=v2.2024-01-04
FEDERATION_CONFIG_VERSION=v2.2023-08-24

View File

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

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="200" height="200">
<!-- Personen in Kreisen -->
<circle cx="30" cy="50" r="15" fill="none" stroke="black" stroke-width="2"/>
<circle cx="50" cy="30" r="15" fill="none" stroke="black" stroke-width="2"/>
<circle cx="70" cy="50" r="15" fill="none" stroke="black" stroke-width="2"/>
<circle cx="50" cy="70" r="15" fill="none" stroke="black" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@ -35,9 +35,9 @@ describe('Sidebar', () => {
expect(wrapper.find('div#component-sidebar').exists()).toBe(true)
})
describe('the genaral section', () => {
it('has six nav-items', () => {
expect(wrapper.findAll('ul').at(0).findAll('.nav-item')).toHaveLength(6)
describe('the general section', () => {
it('has seven nav-items', () => {
expect(wrapper.findAll('ul').at(0).findAll('.nav-item')).toHaveLength(7)
})
it('has nav-item "navigation.overview" in navbar', () => {
@ -60,8 +60,12 @@ describe('Sidebar', () => {
expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.info')
})
it('has nav-item "usersearch" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(5).text()).toContain('navigation.usersearch')
it('has nav-item "navigation.circles" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(5).text()).toContain('navigation.circles')
})
it('has nav-item "navigation.usersearch" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(6).text()).toContain('navigation.usersearch')
})
})

View File

@ -28,6 +28,10 @@
<b-img src="/img/svg/info.svg" height="20" class="svg-icon" />
<span class="ml-2">{{ $t('navigation.info') }}</span>
</b-nav-item>
<b-nav-item to="/circles" class="mb-3" active-class="activeRoute">
<b-img src="/img/svg/circles.svg" height="20" class="svg-icon" />
<span class="ml-2">{{ $t('navigation.circles') }}</span>
</b-nav-item>
<b-nav-item to="/usersearch" active-class="activeRoute">
<b-img src="/img/loupe.png" height="20" />
<span class="ml-2">{{ $t('navigation.usersearch') }}</span>

View File

@ -1,84 +0,0 @@
import { mount } from '@vue/test-utils'
import UserGMSNamingFormat from './UserGMSNamingFormat.vue'
import { toastErrorSpy } from '@test/testSetup'
const mockAPIcall = jest.fn()
const storeCommitMock = jest.fn()
const localVue = global.localVue
describe('UserNamingFormat', () => {
let wrapper
beforeEach(() => {
wrapper = mount(UserGMSNamingFormat, {
mocks: {
$t: (key) => key, // Mocking the translation function
$store: {
state: {
gmsPublishName: null,
},
commit: storeCommitMock,
},
$apollo: {
mutate: mockAPIcall,
},
},
localVue,
propsData: {
selectedOption: 'GMS_PUBLISH_NAME_ALIAS_OR_INITALS',
initialValue: 'GMS_PUBLISH_NAME_ALIAS_OR_INITALS',
attrName: 'gmsPublishName',
successMessage: 'success message',
},
})
})
afterEach(() => {
wrapper.destroy()
})
it('renders the correct dropdown options', () => {
const dropdownItems = wrapper.findAll('.dropdown-item')
expect(dropdownItems.length).toBe(5)
const labels = dropdownItems.wrappers.map((item) => item.text())
expect(labels).toEqual([
'settings.GMS.publish-name.alias-or-initials',
'settings.GMS.publish-name.initials',
'settings.GMS.publish-name.first',
'settings.GMS.publish-name.first-initial',
'settings.GMS.publish-name.name-full',
])
})
it('updates selected option on click', async () => {
const dropdownItem = wrapper.findAll('.dropdown-item').at(3) // Click the fourth item
await dropdownItem.trigger('click')
expect(wrapper.emitted().valueChanged).toBeTruthy()
expect(wrapper.emitted().valueChanged.length).toBe(1)
expect(wrapper.emitted().valueChanged[0]).toEqual(['GMS_PUBLISH_NAME_FIRST_INITIAL'])
})
it('does not update when clicking on already selected option', async () => {
const dropdownItem = wrapper.findAll('.dropdown-item').at(0) // Click the first item (which is already selected)
await dropdownItem.trigger('click')
expect(wrapper.emitted().valueChanged).toBeFalsy()
})
describe('update with error', () => {
beforeEach(async () => {
mockAPIcall.mockRejectedValue({
message: 'Ouch',
})
const dropdownItem = wrapper.findAll('.dropdown-item').at(2) // Click the third item
await dropdownItem.trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
})

View File

@ -1,92 +0,0 @@
<template>
<div class="user-g-m-s-naming-format">
<b-dropdown v-model="selectedOption">
<template slot="button-content">{{ selectedOptionLabel }}</template>
<b-dropdown-item
v-for="option in dropdownOptions"
@click.prevent="update(option)"
:key="option.value"
:value="option.value"
:title="option.title"
>
{{ option.label }}
</b-dropdown-item>
</b-dropdown>
</div>
</template>
<script>
import { updateUserInfos } from '@/graphql/mutations'
export default {
name: 'UserGMSNamingFormat',
props: {
initialValue: { type: String, default: 'GMS_PUBLISH_NAME_ALIAS_OR_INITALS' },
attrName: { type: String },
successMessage: { type: String },
},
data() {
return {
selectedOption: this.initialValue,
dropdownOptions: [
{
label: this.$t('settings.GMS.publish-name.alias-or-initials'),
title: this.$t('settings.GMS.publish-name.alias-or-initials-tooltip'),
value: 'GMS_PUBLISH_NAME_ALIAS_OR_INITALS',
},
{
label: this.$t('settings.GMS.publish-name.initials'),
title: this.$t('settings.GMS.publish-name.initials-tooltip'),
value: 'GMS_PUBLISH_NAME_INITIALS',
},
{
label: this.$t('settings.GMS.publish-name.first'),
title: this.$t('settings.GMS.publish-name.first-tooltip'),
value: 'GMS_PUBLISH_NAME_FIRST',
},
{
label: this.$t('settings.GMS.publish-name.first-initial'),
title: this.$t('settings.GMS.publish-name.first-initial-tooltip'),
value: 'GMS_PUBLISH_NAME_FIRST_INITIAL',
},
{
label: this.$t('settings.GMS.publish-name.name-full'),
title: this.$t('settings.GMS.publish-name.name-full-tooltip'),
value: 'GMS_PUBLISH_NAME_FULL',
},
],
}
},
computed: {
selectedOptionLabel() {
return this.dropdownOptions.find((option) => option.value === this.selectedOption).label
},
},
methods: {
async update(option) {
if (option.value === this.selectedOption) {
return
}
try {
const variables = []
variables[this.attrName] = option.value
await this.$apollo.mutate({
mutation: updateUserInfos,
variables,
})
this.toastSuccess(this.successMessage)
this.selectedOption = option.value
this.$store.commit(this.attrName, option.value)
this.$emit('valueChanged', option.value)
} catch (error) {
this.toastError(error.message)
}
},
},
}
</script>
<style>
.user-g-m-s-naming-format > .dropdown,
.user-g-m-s-naming-format > .dropdown > .dropdown-toggle > ul.dropdown-menu {
width: 100%;
}
</style>

View File

@ -26,9 +26,9 @@ describe('UserNamingFormat', () => {
},
localVue,
propsData: {
selectedOption: 'PUBLISH_NAME_NONE',
initialValue: 'PUBLISH_NAME_NONE',
attrName: 'publishName',
selectedOption: 'PUBLISH_NAME_ALIAS_OR_INITALS',
initialValue: 'PUBLISH_NAME_ALIAS_OR_INITALS',
attrName: 'gmsPublishName',
successMessage: 'success message',
},
})
@ -40,17 +40,15 @@ describe('UserNamingFormat', () => {
it('renders the correct dropdown options', () => {
const dropdownItems = wrapper.findAll('.dropdown-item')
expect(dropdownItems.length).toBe(7)
expect(dropdownItems.length).toBe(5)
const labels = dropdownItems.wrappers.map((item) => item.text())
expect(labels).toEqual([
'settings.publish-name.none',
'settings.publish-name.alias-or-initials',
'settings.publish-name.initials',
'settings.publish-name.first',
'settings.publish-name.first-initial',
'settings.publish-name.last',
'settings.publish-name.last-initial',
'settings.publish-name.full',
'settings.publish-name.name-full',
])
})

View File

@ -20,7 +20,7 @@ import { updateUserInfos } from '@/graphql/mutations'
export default {
name: 'UserNamingFormat',
props: {
initialValue: { type: String, default: 'PUBLISH_NAME_NONE' },
initialValue: { type: String, default: 'PUBLISH_NAME_ALIAS_OR_INITALS' },
attrName: { type: String },
successMessage: { type: String },
},
@ -29,9 +29,9 @@ export default {
selectedOption: this.initialValue,
dropdownOptions: [
{
label: this.$t('settings.publish-name.none'),
title: this.$t('settings.publish-name.none-tooltip'),
value: 'PUBLISH_NAME_NONE',
label: this.$t('settings.publish-name.alias-or-initials'),
title: this.$t('settings.publish-name.alias-or-initials-tooltip'),
value: 'PUBLISH_NAME_ALIAS_OR_INITALS',
},
{
label: this.$t('settings.publish-name.initials'),
@ -49,18 +49,8 @@ export default {
value: 'PUBLISH_NAME_FIRST_INITIAL',
},
{
label: this.$t('settings.publish-name.last'),
title: this.$t('settings.publish-name.last-tooltip'),
value: 'PUBLISH_NAME_LAST',
},
{
label: this.$t('settings.publish-name.last-initial'),
title: this.$t('settings.publish-name.last-initial-tooltip'),
value: 'PUBLISH_NAME_INITIAL_LAST',
},
{
label: this.$t('settings.publish-name.full'),
title: this.$t('settings.publish-name.full-tooltip'),
label: this.$t('settings.publish-name.name-full'),
title: this.$t('settings.publish-name.name-full-tooltip'),
value: 'PUBLISH_NAME_FULL',
},
],
@ -68,7 +58,10 @@ export default {
},
computed: {
selectedOptionLabel() {
return this.dropdownOptions.find((option) => option.value === this.selectedOption).label
const selected = this.dropdownOptions.find((option) => option.value === this.selectedOption)
.label
return selected || this.$t('settings.publish-name.alias-or-initials')
// return this.dropdownOptions.find((option) => option.value === this.selectedOption).label
},
},
methods: {

View File

@ -1,9 +1,10 @@
<template>
<div class="form-user-switch">
<div class="form-user-switch" @click="onClick">
<b-form-checkbox
test="BFormCheckbox"
v-model="value"
name="check-button"
:disabled="disabled"
switch
@change="onChange"
></b-form-checkbox>
@ -19,6 +20,8 @@ export default {
attrName: { type: String },
enabledText: { type: String },
disabledText: { type: String },
disabled: { type: Boolean, default: false },
notAllowedText: { type: String, default: undefined },
},
data() {
return {
@ -27,9 +30,9 @@ export default {
},
methods: {
async onChange() {
if (this.isDisabled) return
const variables = []
variables[this.attrName] = this.value
this.$apollo
.mutate({
mutation: updateUserInfos,
@ -45,6 +48,11 @@ export default {
this.toastError(error.message)
})
},
onClick() {
if (this.notAllowedText && this.disabled) {
this.toastError(this.notAllowedText)
}
},
},
}
</script>

View File

@ -36,7 +36,7 @@ export const updateUserInfos = gql`
$hideAmountGDT: Boolean
$gmsAllowed: Boolean
$humhubAllowed: Boolean
$gmsPublishName: GmsPublishNameType
$gmsPublishName: PublishNameType
$humhubPublishName: PublishNameType
$gmsLocation: Location
$gmsPublishLocation: GmsPublishLocationType

View File

@ -28,6 +28,12 @@ export const authenticateGmsUserSearch = gql`
}
`
export const authenticateHumhubAutoLogin = gql`
query {
authenticateHumhubAutoLogin
}
`
export const transactionsQuery = gql`
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {

View File

@ -5,11 +5,16 @@
"1000thanks": "1000 Dank, weil du bei uns bist!",
"125": "125%",
"85": "85%",
"ExternServices": "Verknüpfte Dienste",
"GDD": "GDD",
"GDT": "GDT",
"GMS": "Gradido Karte",
"Humhub": "Gradido Community",
"GMS": {
"title": "Geo Matching System GMS (in Entwicklung)",
"desc": "Finde Mitglieder aller Communities auf einer Landkarte."
},
"Humhub": {
"title": "Gradido-Kreise",
"desc": "Gemeinsam unterstützen wir einander achtsam in Kreiskultur."
},
"PersonalDetails": "Persönliche Angaben",
"advanced-calculation": "Vorausberechnung",
"asterisks": "****",
@ -25,6 +30,11 @@
}
},
"back": "Zurück",
"circles": {
"headline": "Gemeinsam unterstützen wir einander achtsam in Kreiskultur.",
"text": "In geschützten Räumen können wir frei kommunizieren und kooperieren, ohne auf die sozialen Medien angewiesen zu sein. Mit Klick auf den Button öffnest Du die Kooperationsplattform in einem neuen Browser-Fenster.",
"button": "Gradido-Kreise starten..."
},
"community": {
"admins": "Administratoren",
"choose-another-community": "Eine andere Gemeinschaft auswählen",
@ -268,6 +278,7 @@
"overview": "Übersicht",
"send": "Senden",
"settings": "Einstellung",
"circles": "Kreise",
"support": "Support",
"transactions": "Transaktionen",
"usersearch": "Nutzersuche"
@ -280,6 +291,7 @@
"overview": "Willkommen {name}",
"send": "Sende Gradidos",
"settings": "Einstellungen",
"circles": "Gradido Kreise (Beta)",
"transactions": "Deine Transaktionen",
"usersearch": "Geografische Nutzersuche"
},
@ -294,6 +306,8 @@
"warningText": "Bist du noch da?"
},
"settings": {
"allow-community-services": "Community-Dienste erlauben",
"community": "Community",
"emailInfo": "Kann aktuell noch nicht geändert werden.",
"GMS": {
"disabled": "Daten werden nicht nach GMS exportiert",
@ -302,8 +316,8 @@
"label": "Positionsbestimmung",
"button": "Klick mich!"
},
"location-format": "Positionstyp",
"naming-format": "Namensformat im GMS",
"location-format": "Position auf Karte anzeigen:",
"naming-format": "Namen anzeigen:",
"publish-location": {
"exact": "Genaue Position",
"approximate": "Ungefähre Position",
@ -311,30 +325,19 @@
"updated": "Positionstyp für GMS aktualisiert"
},
"publish-name": {
"alias-or-initials": "Benutzername oder Initialen",
"alias-or-initials-tooltip": "Benutzername, falls vorhanden, oder die Initialen von Vorname und Nachname",
"first": "Vorname",
"first-tooltip": "Nur der Vornamen",
"first-initial": "Vorname und Initiale",
"first-initial-tooltip": "Vornamen plus Anfangsbuchstabe des Nachnamens",
"initials": "Initialen",
"initials-tooltip": "Initialen von Vor- und Nachname unabhängig von der Existenz des Benutzernamens",
"name-full": "Ganzer Name",
"name-full-tooltip": "Vollständiger Name: Vorname plus Nachname",
"updated": "Namensformat für GMS aktualisiert"
},
"switch": "Erlaubnis Daten nach GMS zu exportieren."
}
},
"hideAmountGDD": "Dein GDD Betrag ist versteckt.",
"hideAmountGDT": "Dein GDT Betrag ist versteckt.",
"humhub": {
"delete-disabled": "Das Benutzerkonto kann nur im Profil-Menü der Kooperationsplattform gelöscht werden.",
"disabled": "Daten werden nicht in die Gradido Community exportiert",
"enabled": "Daten werden in die Gradido Community exportiert",
"naming-format": "Namensformat in der Gradido Community",
"naming-format": "Namen anzeigen:",
"publish-name": {
"updated": "Namensformat für die Gradido Community aktualisiert."
},
"switch": "Erlaubnis Daten in die Gradido Community zu exportieren."
}
},
"info": "Transaktionen können nun per Benutzername oder E-Mail-Adresse getätigt werden.",
"language": {
@ -374,20 +377,16 @@
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen."
},
"publish-name": {
"none": "Keine",
"none-tooltip": "Vorname und Nachname bleiben leer",
"alias-or-initials": "Benutzername oder Initialen",
"alias-or-initials-tooltip": "Benutzername, falls vorhanden, oder die Initialen von Vorname und Nachname",
"first": "Vorname",
"first-tooltip": "Nur der Vornamen, z.B. Max",
"first-initial": "Vorname und Initiale",
"first-initial-tooltip": "Vornamen plus Anfangsbuchstabe des Nachnamens, z.B. Max M.",
"last": "Nachname",
"last-tooltip": "Nur der Nachname, z.B. Mustermann",
"last-initial": "Initiale und Nachname",
"last-initial-tooltip": "Anfangsbuchstabe des Vornamen plus Nachname, z.B. M. Mustermann",
"first-tooltip": "Nur der Vornamen",
"first-initial": "Vorname und Initial",
"first-initial-tooltip": "Vornamen plus Anfangsbuchstabe des Nachnamens",
"initials": "Initialen",
"initials-tooltip": "Nur die Initialen von Vor- und Nachname, z.B. M. M.",
"full": "Ganzer Name",
"full-tooltip": "Vollständiger Name: Vorname plus Nachname, z.B. Max Mustermann"
"initials-tooltip": "Initialen von Vor- und Nachname unabhängig von der Existenz des Benutzernamens",
"name-full": "Vorname und Nachname",
"name-full-tooltip": "Vollständiger Name: Vorname plus Nachname"
},
"showAmountGDD": "Dein GDD Betrag ist sichtbar.",
"showAmountGDT": "Dein GDT Betrag ist sichtbar.",

View File

@ -5,11 +5,16 @@
"1000thanks": "1000 thanks for being with us!",
"125": "125%",
"85": "85%",
"ExternServices": "Extern Services",
"GDD": "GDD",
"GDT": "GDT",
"GMS": "Gradido Map",
"Humhub": "Gradido Community",
"GMS": {
"title": "Geo Matching System GMS (in develop)",
"desc": "Find members of all communities on a map."
},
"Humhub": {
"title": "Gradido-circles",
"desc": "Together we support each other - mindful in circle culture."
},
"PersonalDetails": "Personal details",
"advanced-calculation": "Advanced calculation",
"asterisks": "****",
@ -25,6 +30,11 @@
}
},
"back": "Back",
"circles": {
"headline": "Together we support each other - mindful in circle culture.",
"text": "We can communicate and collaborate freely in protected spaces without having to rely on social media. Click on the button to open the collaboration platform in a new browser window.",
"button": "Gradido circles start..."
},
"community": {
"admins": "Administrators",
"choose-another-community": "Choose another community",
@ -268,6 +278,7 @@
"overview": "Overview",
"send": "Send",
"settings": "Settings",
"circles": "Circle",
"support": "Support",
"transactions": "Transactions",
"usersearch": "Geographical User Search"
@ -281,6 +292,7 @@
"send": "Send Gradidos",
"settings": "Settings",
"transactions": "Your transactions",
"circles": "Gradido Circles (Beta)",
"usersearch": "Geographical User Search"
},
"qrCode": "QR Code",
@ -294,6 +306,8 @@
"warningText": "Are you still there?"
},
"settings": {
"allow-community-services": "Allow Community Services",
"community": "Community",
"emailInfo": "Cannot be changed at this time.",
"GMS": {
"disabled": "Data not exported to GMS",
@ -302,8 +316,8 @@
"label": "pinpoint location",
"button": "click me!"
},
"location-format": "location type",
"naming-format": "Format of name in GMS",
"location-format": "Show position on map:",
"naming-format": "Show Name:",
"publish-location": {
"exact": "exact position",
"approximate": "approximate position",
@ -311,30 +325,19 @@
"updated": "format of location for GMS updated"
},
"publish-name": {
"alias-or-initials": "Username or initials",
"alias-or-initials-tooltip": "username if exists or Initials of firstname and lastname",
"first": "firstname",
"first-tooltip": "the firstname only",
"first-initial": "firstname and initial",
"first-initial-tooltip": "firstname plus initial of lastname",
"initials": "Initials of firstname and lastname independent if username exists",
"initials-tooltip": "Initials of firstname and lastname independent if username exists",
"name-full": "fullname",
"name-full-tooltip": "fullname: firstname plus lastname",
"updated": "format of name for GMS updated"
},
"switch": "Allow data export to GMS"
}
},
"hideAmountGDD": "Your GDD amount is hidden.",
"hideAmountGDT": "Your GDT amount is hidden.",
"humhub": {
"delete-disabled": "The user account can only be deleted in the profile menu of the cooperation platform.",
"disabled": "Data not exported into the Gradido Community",
"enabled": "Data exported into the Gradido Community",
"naming-format": "Format of name in the Gradido Community",
"naming-format": "Show Name:",
"publish-name": {
"updated": "Format of name for the Gradido Community updated."
},
"switch": "Allow data export into the Gradido Community."
}
},
"info": "Transactions can now be made by username or email address.",
"language": {
@ -374,20 +377,16 @@
"subtitle": "If you have forgotten your password, you can reset it here."
},
"publish-name": {
"none": "None",
"none-tooltip": "first name and last name are empty",
"first": "first name",
"first-tooltip": "the first name only",
"first-initial": "first name and initial",
"first-initial-tooltip": "first name plus initial of last name",
"last": "last name",
"last-tooltip": "last name only",
"last-initial": "initial and last name",
"last-initial-tooltip": "First letter of the first name plus last name",
"initials": "initials",
"initials-tooltip": "Initials of first name and last name",
"full": "full name",
"full-tooltip": "full name: firstname plus lastname"
"alias-or-initials": "Username or initials (Default)",
"alias-or-initials-tooltip": "username if exists or Initials of firstname and lastname",
"first": "firstname",
"first-tooltip": "the firstname only",
"first-initial": "firstname and initial",
"first-initial-tooltip": "firstname plus initial of lastname",
"initials": "Initials",
"initials-tooltip": "Initials of firstname and lastname independent if username exists",
"name-full": "firstname and lastname",
"name-full-tooltip": "fullname: firstname plus lastname"
},
"showAmountGDD": "Your GDD amount is visible.",
"showAmountGDT": "Your GDT amount is visible.",

View File

@ -0,0 +1,71 @@
<template>
<div class="circles">
<b-container class="bg-white appBoxShadow gradido-border-radius p-4 mt--3">
<div class="h3">{{ $t('circles.headline') }}</div>
<div class="my-4 text-small">
<span v-for="(line, lineNumber) of $t('circles.text').split('\n')" v-bind:key="lineNumber">
{{ line }}
<br />
</span>
</div>
<b-row class="my-5">
<b-col cols="12">
<div class="text-lg-right">
<b-button
v-if="this.humhubAllowed"
variant="gradido"
:disabled="this.enableButton === false"
@click="authenticateHumhubAutoLogin"
target="_blank"
>
{{ $t('circles.button') }}
</b-button>
<RouterLink v-else to="/settings/extern">
<b-button variant="gradido">
{{ $t('circles.button') }}
</b-button>
</RouterLink>
</div>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import { authenticateHumhubAutoLogin } from '@/graphql/queries'
export default {
name: 'Circles',
data() {
return {
enableButton: true,
}
},
computed: {
humhubAllowed() {
return this.$store.state.humhubAllowed
},
},
methods: {
async authenticateHumhubAutoLogin() {
this.enableButton = false
this.humhubUri = null
this.$apollo
.query({
query: authenticateHumhubAutoLogin,
fetchPolicy: 'network-only',
})
.then(async (result) => {
window.open(result.data.authenticateHumhubAutoLogin, '_blank')
this.enableButton = true
})
.catch(() => {
// this.toastError('authenticateHumhubAutoLogin failed!')
this.enableButton = true
// something went wrong with login link so we disable humhub
this.$store.commit('humhubAllowed', false)
this.$router.push('/settings/extern')
})
},
},
}
</script>

View File

@ -22,12 +22,17 @@ describe('Settings', () => {
email: 'john.doe@test.com',
language: 'en',
newsletterState: false,
gmsAllowed: false,
humhubAllowed: false,
},
commit: storeCommitMock,
},
$apollo: {
mutate: mockAPIcall,
},
$route: {
params: {},
},
}
const Wrapper = () => {

View File

@ -1,7 +1,7 @@
<template>
<div class="card bg-white gradido-border-radius appBoxShadow p-4 mt--3">
<b-tabs content-class="mt-3">
<b-tab :title="$t('PersonalDetails')" active>
<b-tabs v-model="tabIndex" content-class="mt-3">
<b-tab :title="$t('PersonalDetails')">
<div class="h2">{{ $t('PersonalDetails') }}</div>
<div class="my-4 text-small">
{{ $t('settings.info') }}
@ -79,35 +79,64 @@
</b-col>
</b-row>
</b-tab>
<div v-if="isExternService">
<b-tab :title="$t('ExternServices')">
<div class="h2">{{ $t('ExternServices') }}</div>
<div v-if="isGMS">
<div class="h3">{{ $t('GMS') }}</div>
<b-row class="mb-3">
<div v-if="isCommunityService">
<b-tab class="community-service-tabs" :title="$t('settings.community')">
<div class="h2">{{ $t('settings.allow-community-services') }}</div>
<div v-if="isHumhub" class="mt-3">
<b-row>
<b-col cols="12" md="6" lg="6">
{{ $t('settings.GMS.switch') }}
<div class="text-small">
{{ gmsAllowed ? $t('settings.GMS.enabled') : $t('settings.GMS.disabled') }}
</div>
<div class="h3">{{ $t('Humhub.title') }}</div>
</b-col>
<b-col cols="12" md="6" lg="6" class="text-right">
<user-settings-switch
@valueChanged="humhubStateSwitch"
:initialValue="$store.state.humhubAllowed"
:attrName="'humhubAllowed'"
:disabled="isHumhubActivated"
:enabledText="$t('settings.humhub.enabled')"
:disabledText="$t('settings.humhub.disabled')"
:notAllowedText="$t('settings.humhub.delete-disabled')"
/>
</b-col>
</b-row>
<div class="h4">{{ $t('Humhub.desc') }}</div>
<b-row v-if="humhubAllowed" class="mb-4 humhub-publish-name-row">
<b-col cols="12" md="6" lg="6">
{{ $t('settings.humhub.naming-format') }}
</b-col>
<b-col cols="12" md="6" lg="6">
<user-naming-format
:initialValue="$store.state.humhubPublishName"
:attrName="'humhubPublishName'"
:successMessage="$t('settings.humhub.publish-name.updated')"
/>
</b-col>
</b-row>
</div>
<div v-if="isGMS" class="mt-3">
<b-row>
<b-col cols="12" md="6" lg="6">
<div class="h3 text-muted">{{ $t('GMS.title') }}</div>
</b-col>
<b-col cols="12" md="6" lg="6" class="text-right">
<user-settings-switch
@valueChanged="gmsStateSwitch"
:initialValue="$store.state.gmsAllowed"
:attrName="'gmsAllowed'"
:disabled="true"
:enabledText="$t('settings.GMS.enabled')"
:disabledText="$t('settings.GMS.disabled')"
/>
</b-col>
</b-row>
<div class="h4 mt-3 text-muted">{{ $t('GMS.desc') }}</div>
<div v-if="gmsAllowed">
<b-row class="mb-4">
<b-col cols="12" md="6" lg="6">
{{ $t('settings.GMS.naming-format') }}
</b-col>
<b-col cols="12" md="6" lg="6">
<user-g-m-s-naming-format
<user-naming-format
:initialValue="$store.state.gmsPublishName"
:attrName="'gmsPublishName'"
:successMessage="$t('settings.GMS.publish-name.updated')"
@ -132,40 +161,6 @@
</b-row>
</div>
</div>
<div v-if="isHumhub">
<div class="h3">{{ $t('Humhub') }}</div>
<b-row class="mb-3">
<b-col cols="12" md="6" lg="6">
{{ $t('settings.humhub.switch') }}
<div class="text-small">
{{
humhubAllowed ? $t('settings.humhub.enabled') : $t('settings.humhub.disabled')
}}
</div>
</b-col>
<b-col cols="12" md="6" lg="6" class="text-right">
<user-settings-switch
@valueChanged="humhubStateSwitch"
:initialValue="$store.state.humhubAllowed"
:attrName="'humhubAllowed'"
:enabledText="$t('settings.humhub.enabled')"
:disabledText="$t('settings.humhub.disabled')"
/>
</b-col>
</b-row>
<b-row v-if="humhubAllowed" class="mb-4">
<b-col cols="12" md="6" lg="6">
{{ $t('settings.humhub.naming-format') }}
</b-col>
<b-col cols="12" md="6" lg="6">
<user-naming-format
:initialValue="$store.state.humhubPublishName"
:attrName="'humhubPublishName'"
:successMessage="$t('settings.humhub.publish-name.updated')"
/>
</b-col>
</b-row>
</div>
</b-tab>
</div>
</b-tabs>
@ -180,7 +175,6 @@
</template>
<script>
import UserNamingFormat from '@/components/UserSettings/UserNamingFormat'
import UserGMSNamingFormat from '@/components/UserSettings/UserGMSNamingFormat'
import UserGMSLocationFormat from '@/components/UserSettings/UserGMSLocationFormat'
import UserGMSLocation from '@/components/UserSettings/UserGMSLocation'
import UserName from '@/components/UserSettings/UserName.vue'
@ -195,7 +189,6 @@ export default {
name: 'Profile',
components: {
UserNamingFormat,
UserGMSNamingFormat,
UserGMSLocationFormat,
UserGMSLocation,
UserName,
@ -222,6 +215,10 @@ export default {
} = state
const username = this.$store.state.username || ''
let tabIndex = 0
if (this.$route.params.tabAlias === 'extern') {
tabIndex = 1
}
return {
darkMode,
@ -234,6 +231,7 @@ export default {
humhubAllowed,
mutation: '',
variables: {},
tabIndex,
}
},
@ -242,14 +240,17 @@ export default {
const { firstName, lastName } = this.$store.state
return firstName === this.firstName && lastName === this.lastName
},
isExternService() {
isHumhubActivated() {
return this.humhubAllowed
},
isCommunityService() {
return this.isGMS || this.isHumhub
},
isGMS() {
return CONFIG.GMS_ACTIVE
},
isHumhub() {
return CONFIG.HUMHUB_ACTIVE && this.username
return CONFIG.HUMHUB_ACTIVE
},
},
// TODO: watch: {
@ -286,6 +287,9 @@ export default {
}
</script>
<style>
.community-service-tabs {
min-height: 315px;
}
.card-border-radius {
border-radius: 0px 5px 5px 0px !important;
}

View File

@ -49,8 +49,8 @@ describe('router', () => {
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
})
it('has 20 routes defined', () => {
expect(routes).toHaveLength(20)
it('has 21 routes defined', () => {
expect(routes).toHaveLength(21)
})
describe('overview', () => {

View File

@ -88,6 +88,14 @@ const routes = [
pageTitle: 'usersearch',
},
},
{
path: '/circles',
component: () => import('@/pages/Circles'),
meta: {
requiresAuth: true,
pageTitle: 'circles',
},
},
// {
// path: '/storys',
// component: () => import('@/pages/TopStorys'),
@ -103,7 +111,7 @@ const routes = [
// },
// },
{
path: '/settings',
path: '/settings/:tabAlias?',
component: () => import('@/pages/Settings'),
meta: {
requiresAuth: true,