mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
rework PR comments
This commit is contained in:
parent
13d79fd8b7
commit
411e03c843
@ -41,7 +41,7 @@ import Paginated from '@arg/Paginated'
|
|||||||
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { communityUser } from '@/util/communityUser'
|
import { communityUser } from '@/util/communityUser'
|
||||||
import { activationLink, printTimeDuration } from './UserResolver'
|
import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
@ -403,6 +403,7 @@ export class AdminResolver {
|
|||||||
throw new Error('Contribution not found for given id.')
|
throw new Error('Contribution not found for given id.')
|
||||||
}
|
}
|
||||||
contribution.contributionStatus = ContributionStatus.DELETED
|
contribution.contributionStatus = ContributionStatus.DELETED
|
||||||
|
await contribution.save()
|
||||||
const res = await contribution.softRemove()
|
const res = await contribution.softRemove()
|
||||||
return !!res
|
return !!res
|
||||||
}
|
}
|
||||||
@ -514,16 +515,21 @@ export class AdminResolver {
|
|||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const emailContact = await UserContact.findOne({ email: email })
|
// const user = await dbUser.findOne({ id: emailContact.userId })
|
||||||
if (!emailContact) {
|
const user = await findUserByEmail(email)
|
||||||
logger.error(`Could not find UserContact with email: ${email}`)
|
|
||||||
throw new Error(`Could not find UserContact with email: ${email}`)
|
|
||||||
}
|
|
||||||
const user = await dbUser.findOne({ id: emailContact.userId })
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.error(`Could not find User to emailContact: ${email}`)
|
logger.error(`Could not find User to emailContact: ${email}`)
|
||||||
throw new Error(`Could not find User to emailContact: ${email}`)
|
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||||
}
|
}
|
||||||
|
if (user.deletedAt) {
|
||||||
|
logger.error(`User with emailContact: ${email} is deleted.`)
|
||||||
|
throw new Error(`User with emailContact: ${email} is deleted.`)
|
||||||
|
}
|
||||||
|
const emailContact = user.emailContact
|
||||||
|
if (emailContact.deletedAt) {
|
||||||
|
logger.error(`The emailContact: ${email} of htis User is deleted.`)
|
||||||
|
throw new Error(`The emailContact: ${email} of htis User is deleted.`)
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendAccountActivationEmail({
|
const emailSent = await sendAccountActivationEmail({
|
||||||
|
|||||||
@ -283,7 +283,10 @@ export class TransactionLinkResolver {
|
|||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
const linkedUser = await dbUser.findOneOrFail(
|
||||||
|
{ id: transactionLink.userId },
|
||||||
|
{ relations: ['user'] },
|
||||||
|
)
|
||||||
|
|
||||||
if (user.id === linkedUser.id) {
|
if (user.id === linkedUser.id) {
|
||||||
throw new Error('Cannot redeem own transaction link.')
|
throw new Error('Cannot redeem own transaction link.')
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import Decimal from 'decimal.js-light'
|
|||||||
import { BalanceResolver } from './BalanceResolver'
|
import { BalanceResolver } from './BalanceResolver'
|
||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
import { UserContact } from '@entity/UserContact'
|
import { UserContact } from '@entity/UserContact'
|
||||||
|
import { findUserByEmail } from './UserResolver'
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
@ -294,13 +295,15 @@ export class TransactionResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate recipient user
|
// validate recipient user
|
||||||
|
const recipientUser = await findUserByEmail(email)
|
||||||
|
/*
|
||||||
const emailContact = await UserContact.findOne({ email }, { withDeleted: true })
|
const emailContact = await UserContact.findOne({ email }, { withDeleted: true })
|
||||||
if (!emailContact) {
|
if (!emailContact) {
|
||||||
logger.error(`Could not find UserContact with email: ${email}`)
|
logger.error(`Could not find UserContact with email: ${email}`)
|
||||||
throw new Error(`Could not find UserContact with email: ${email}`)
|
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
const recipientUser = await dbUser.findOne({ id: emailContact.userId })
|
// const recipientUser = await dbUser.findOne({ id: emailContact.userId })
|
||||||
if (!recipientUser) {
|
if (!recipientUser) {
|
||||||
logger.error(`unknown recipient to UserContact: email=${email}`)
|
logger.error(`unknown recipient to UserContact: email=${email}`)
|
||||||
throw new Error('unknown recipient')
|
throw new Error('unknown recipient')
|
||||||
@ -309,6 +312,7 @@ export class TransactionResolver {
|
|||||||
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||||
throw new Error('The recipient account was deleted')
|
throw new Error('The recipient account was deleted')
|
||||||
}
|
}
|
||||||
|
const emailContact = recipientUser.emailContact
|
||||||
if (!emailContact.emailChecked) {
|
if (!emailContact.emailChecked) {
|
||||||
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
||||||
throw new Error('The recipient account is not activated')
|
throw new Error('The recipient account is not activated')
|
||||||
|
|||||||
@ -873,7 +873,7 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findUserByEmail(email: string): Promise<DbUser> {
|
export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||||
const dbUserContact = await DbUserContact.findOneOrFail(
|
const dbUserContact = await DbUserContact.findOneOrFail(
|
||||||
{ email: email },
|
{ email: email },
|
||||||
{ withDeleted: true, relations: ['user'] },
|
{ withDeleted: true, relations: ['user'] },
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export const userFactory = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get last changes of user from database
|
// get last changes of user from database
|
||||||
dbUser = await User.findOneOrFail({ id }, { withDeleted: true })
|
// dbUser = await User.findOneOrFail({ id }, { withDeleted: true })
|
||||||
|
|
||||||
return dbUser
|
return dbUser
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,159 +1,159 @@
|
|||||||
# Introduction of Gradido-ID
|
# Introduction of Gradido-ID
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
The introduction of the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage.
|
The introduction of the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage.
|
||||||
|
|
||||||
Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account.
|
Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account.
|
||||||
|
|
||||||
## Definition
|
## Definition
|
||||||
|
|
||||||
The formalized definition of the Gradido-ID can be found in the document [BenutzerVerwaltung#Gradido-ID](../BusinessRequirements/BenutzerVerwaltung#Gradido-ID).
|
The formalized definition of the Gradido-ID can be found in the document [BenutzerVerwaltung#Gradido-ID](../BusinessRequirements/BenutzerVerwaltung#Gradido-ID).
|
||||||
|
|
||||||
## Steps of Introduction
|
## Steps of Introduction
|
||||||
|
|
||||||
To Introduce the Gradido-ID there are several steps necessary. The first step is to define a proper database schema with additional columns and tables followed by data migration steps to add or initialize the new columns and tables by keeping valid data at all.
|
To Introduce the Gradido-ID there are several steps necessary. The first step is to define a proper database schema with additional columns and tables followed by data migration steps to add or initialize the new columns and tables by keeping valid data at all.
|
||||||
|
|
||||||
The second step is to decribe all concerning business logic processes, which have to be adapted by introducing the Gradido-ID.
|
The second step is to decribe all concerning business logic processes, which have to be adapted by introducing the Gradido-ID.
|
||||||
|
|
||||||
### Database-Schema
|
### Database-Schema
|
||||||
|
|
||||||
#### Users-Table
|
#### Users-Table
|
||||||
|
|
||||||
The entity users has to be changed by adding the following columns.
|
The entity users has to be changed by adding the following columns.
|
||||||
|
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
| ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------- |
|
| ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
| gradidoID | String | technical unique key of the user as UUID (version 4) |
|
| gradidoID | String | technical unique key of the user as UUID (version 4) |
|
||||||
| alias | String | a business unique key of the user |
|
| alias | String | a business unique key of the user |
|
||||||
| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... |
|
| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... |
|
||||||
| emailID | int | technical foreign key to the entry with type Email and contactChannel=maincontact of the new entity UserContacts |
|
| emailID | int | technical foreign key to the entry with type Email and contactChannel=maincontact of the new entity UserContacts |
|
||||||
|
|
||||||
##### Email vs emailID
|
##### Email vs emailID
|
||||||
|
|
||||||
The existing column `email`, will now be changed to the primary email contact, which will be stored as a contact entry in the new `UserContacts` table. It is necessary to decide if the content of the `email `will be changed to the foreign key `emailID `to the contact entry with the email address or if the email itself will be kept as a denormalized and duplicate value in the `users `table.
|
The existing column `email`, will now be changed to the primary email contact, which will be stored as a contact entry in the new `UserContacts` table. It is necessary to decide if the content of the `email `will be changed to the foreign key `emailID `to the contact entry with the email address or if the email itself will be kept as a denormalized and duplicate value in the `users `table.
|
||||||
|
|
||||||
The preferred and proper solution will be to add a new column `Users.emailId `as foreign key to the `UsersContact `entry and delete the `Users.email` column after the migration of the email address in the `UsersContact `table.
|
The preferred and proper solution will be to add a new column `Users.emailId `as foreign key to the `UsersContact `entry and delete the `Users.email` column after the migration of the email address in the `UsersContact `table.
|
||||||
|
|
||||||
#### new UserContacts-Table
|
#### new UserContacts-Table
|
||||||
|
|
||||||
A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses.
|
A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses.
|
||||||
|
|
||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
| --------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| id | int | the technical key of a contact entity |
|
| id | int | the technical key of a contact entity |
|
||||||
| type | int | Defines the type of contact entry as enum: Email, Phone, etc |
|
| type | int | Defines the type of contact entry as enum: Email, Phone, etc |
|
||||||
| userID | int | Defines the foreign key to the `Users` table |
|
| userID | int | Defines the foreign key to the `Users` table |
|
||||||
| email | String | defines the address of a contact entry of type Email |
|
| email | String | defines the address of a contact entry of type Email |
|
||||||
| emailVerificationCode | unsinged bigint(20) | unique code to verify email or password reset |
|
| emailVerificationCode | unsinged bigint(20) | unique code to verify email or password reset |
|
||||||
| emailOptInType | int | REGISTER=1, RESET_PASSWORD=2 |
|
| emailOptInType | int | REGISTER=1, RESET_PASSWORD=2 |
|
||||||
| emailResendCount | int | counter how often the email was resend |
|
| emailResendCount | int | counter how often the email was resend |
|
||||||
| emailChecked | boolean | flag if email is verified and confirmed |
|
| emailChecked | boolean | flag if email is verified and confirmed |
|
||||||
| createdAt | DateTime | point of time the Contact was created |
|
| createdAt | DateTime | point of time the Contact was created |
|
||||||
| updatedAt | DateTime | point of time the Contact was updated |
|
| updatedAt | DateTime | point of time the Contact was updated |
|
||||||
| deletedAt | DateTime | point of time the Contact was soft deleted |
|
| deletedAt | DateTime | point of time the Contact was soft deleted |
|
||||||
| phone | String | defines the address of a contact entry of type Phone |
|
| phone | String | defines the address of a contact entry of type Phone |
|
||||||
| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... |
|
| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... |
|
||||||
|
|
||||||
### Database-Migration
|
### Database-Migration
|
||||||
|
|
||||||
After the adaption of the database schema and to keep valid consistent data, there must be several steps of data migration to initialize the new and changed columns and tables.
|
After the adaption of the database schema and to keep valid consistent data, there must be several steps of data migration to initialize the new and changed columns and tables.
|
||||||
|
|
||||||
#### Initialize GradidoID
|
#### Initialize GradidoID
|
||||||
|
|
||||||
In a one-time migration create for each entry of the `Users `tabel an unique UUID (version4).
|
In a one-time migration create for each entry of the `Users `tabel an unique UUID (version4).
|
||||||
|
|
||||||
#### Primary Email Contact
|
#### Primary Email Contact
|
||||||
|
|
||||||
In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email`, select from the table `login_email_opt_in` the entry with the `login_email_opt_in.user_id` = `Users.id` and create a new entry in the `UsersContact `table, by initializing the contact-values with:
|
In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email`, select from the table `login_email_opt_in` the entry with the `login_email_opt_in.user_id` = `Users.id` and create a new entry in the `UsersContact `table, by initializing the contact-values with:
|
||||||
|
|
||||||
* id = new technical key
|
* id = new technical key
|
||||||
* type = Enum-Email
|
* type = Enum-Email
|
||||||
* userID = `Users.id`
|
* userID = `Users.id`
|
||||||
* email = `Users.email`
|
* email = `Users.email`
|
||||||
* emailVerifyCode = `login_email_opt_in.verification_code`
|
* emailVerifyCode = `login_email_opt_in.verification_code`
|
||||||
* emailOptInType = `login_email_opt_in.email_opt_in_type_id`
|
* emailOptInType = `login_email_opt_in.email_opt_in_type_id`
|
||||||
* emailResendCount = `login_email_opt_in.resent_count`
|
* emailResendCount = `login_email_opt_in.resent_count`
|
||||||
* emailChecked = `Users.emailChecked`
|
* emailChecked = `Users.emailChecked`
|
||||||
* createdAt = `login_email_opt_in.created_at`
|
* createdAt = `login_email_opt_in.created_at`
|
||||||
* updatedAt = `login_email_opt_in.updated_at`
|
* updatedAt = `login_email_opt_in.updated_at`
|
||||||
* phone = null
|
* phone = null
|
||||||
* usedChannel = Enum-"main contact"
|
* usedChannel = Enum-"main contact"
|
||||||
|
|
||||||
and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1`
|
and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1`
|
||||||
|
|
||||||
After this one-time migration and a verification, which ensures that all data are migrated, then the columns `Users.email`, `Users.emailChecked`, `Users.emailHash` and the table `login_email_opt_in` can be deleted.
|
After this one-time migration and a verification, which ensures that all data are migrated, then the columns `Users.email`, `Users.emailChecked`, `Users.emailHash` and the table `login_email_opt_in` can be deleted.
|
||||||
|
|
||||||
### Adaption of BusinessLogic
|
### Adaption of BusinessLogic
|
||||||
|
|
||||||
The following logic or business processes has to be adapted for introducing the Gradido-ID
|
The following logic or business processes has to be adapted for introducing the Gradido-ID
|
||||||
|
|
||||||
#### Read-Write Access of Users-Table especially Email
|
#### Read-Write Access of Users-Table especially Email
|
||||||
|
|
||||||
The ORM mapping has to be adapted to the changed and new database schema.
|
The ORM mapping has to be adapted to the changed and new database schema.
|
||||||
|
|
||||||
#### Registration Process
|
#### Registration Process
|
||||||
|
|
||||||
The logic of the registration process has to be adapted by
|
The logic of the registration process has to be adapted by
|
||||||
|
|
||||||
* initializing the `Users.userID` with a unique UUID
|
* initializing the `Users.userID` with a unique UUID
|
||||||
* creating a new `UsersContact `entry with the given email address and *maincontact* as `usedChannel `
|
* creating a new `UsersContact `entry with the given email address and *maincontact* as `usedChannel `
|
||||||
* set `emailID `in the `Users `table as foreign key to the new `UsersContact `entry
|
* set `emailID `in the `Users `table as foreign key to the new `UsersContact `entry
|
||||||
* set `Users.passphraseEncrpytionType = 2` and encrypt the passphrase with the `Users.userID` instead of the `UsersContact.email`
|
* set `Users.passphraseEncrpytionType = 2` and encrypt the passphrase with the `Users.userID` instead of the `UsersContact.email`
|
||||||
|
|
||||||
#### Login Process
|
#### Login Process
|
||||||
|
|
||||||
The logic of the login process has to be adapted by
|
The logic of the login process has to be adapted by
|
||||||
|
|
||||||
* search the users data by reading the `Users `and the `UsersContact` table with the email (or alias as soon as the user can maintain his profil with an alias) as input
|
* search the users data by reading the `Users `and the `UsersContact` table with the email (or alias as soon as the user can maintain his profil with an alias) as input
|
||||||
* depending on the `Users.passphraseEncryptionType` decrypt the stored password
|
* depending on the `Users.passphraseEncryptionType` decrypt the stored password
|
||||||
* = 1 : with the email
|
* = 1 : with the email
|
||||||
* = 2 : with the userID
|
* = 2 : with the userID
|
||||||
|
|
||||||
#### Password En/Decryption
|
#### Password En/Decryption
|
||||||
|
|
||||||
The logic of the password en/decryption has to be adapted by encapsulate the logic to be controlled with an input parameter. The input parameter can be the email or the userID.
|
The logic of the password en/decryption has to be adapted by encapsulate the logic to be controlled with an input parameter. The input parameter can be the email or the userID.
|
||||||
|
|
||||||
#### Change Password Process
|
#### Change Password Process
|
||||||
|
|
||||||
The logic of change password has to be adapted by
|
The logic of change password has to be adapted by
|
||||||
|
|
||||||
* if the `Users.passphraseEncryptionType` = 1, then
|
* if the `Users.passphraseEncryptionType` = 1, then
|
||||||
|
|
||||||
* read the users email address from the `UsersContact `table
|
* read the users email address from the `UsersContact `table
|
||||||
* give the email address as input for the password decryption of the existing password
|
* give the email address as input for the password decryption of the existing password
|
||||||
* use the `Users.userID` as input for the password encryption for the new password
|
* use the `Users.userID` as input for the password encryption for the new password
|
||||||
* change the `Users.passphraseEnrycptionType` to the new value =2
|
* change the `Users.passphraseEnrycptionType` to the new value =2
|
||||||
* if the `Users.passphraseEncryptionType` = 2, then
|
* if the `Users.passphraseEncryptionType` = 2, then
|
||||||
|
|
||||||
* give the `Users.userID` as input for the password decryption of the existing password
|
* give the `Users.userID` as input for the password decryption of the existing password
|
||||||
* use the `Users.userID` as input for the password encryption fo the new password
|
* use the `Users.userID` as input for the password encryption fo the new password
|
||||||
|
|
||||||
#### Search- and Access Logic
|
#### Search- and Access Logic
|
||||||
|
|
||||||
A new logic has to be introduced to search the user identity per different input values. That means searching the user data must be possible by
|
A new logic has to be introduced to search the user identity per different input values. That means searching the user data must be possible by
|
||||||
|
|
||||||
* searching per email (only with maincontact as contactchannel)
|
* searching per email (only with maincontact as contactchannel)
|
||||||
* searching per userID
|
* searching per userID
|
||||||
* searching per alias
|
* searching per alias
|
||||||
|
|
||||||
#### Identity-Mapping
|
#### Identity-Mapping
|
||||||
|
|
||||||
A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output:
|
A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output:
|
||||||
|
|
||||||
* email -> userID
|
* email -> userID
|
||||||
* email -> gradidoID
|
* email -> gradidoID
|
||||||
* email -> alias
|
* email -> alias
|
||||||
* userID -> gradidoID
|
* userID -> gradidoID
|
||||||
* userID -> email
|
* userID -> email
|
||||||
* userID -> alias
|
* userID -> alias
|
||||||
* alias -> gradidoID
|
* alias -> gradidoID
|
||||||
* alias -> email
|
* alias -> email
|
||||||
* alias -> userID
|
* alias -> userID
|
||||||
* gradidoID -> email
|
* gradidoID -> email
|
||||||
* gradidoID -> userID
|
* gradidoID -> userID
|
||||||
* gradidoID -> alias
|
* gradidoID -> alias
|
||||||
|
|
||||||
#### GDT-Access
|
#### GDT-Access
|
||||||
|
|
||||||
To use the GDT-servers api the used identifier for GDT has to be switch from email to userID.
|
To use the GDT-servers api the used identifier for GDT has to be switch from email to userID.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user