mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2072-feature-usecase-contribution-messaging
This commit is contained in:
commit
f79b1032b7
15
admin/src/graphql/communityStatistics.js
Normal file
15
admin/src/graphql/communityStatistics.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const communityStatistics = gql`
|
||||||
|
query {
|
||||||
|
communityStatistics {
|
||||||
|
totalUsers
|
||||||
|
activeUsers
|
||||||
|
deletedUsers
|
||||||
|
totalGradidoCreated
|
||||||
|
totalGradidoDecayed
|
||||||
|
totalGradidoAvailable
|
||||||
|
totalGradidoUnbookedDecayed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -19,6 +19,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
"apollo-server-express": "^2.25.2",
|
"apollo-server-express": "^2.25.2",
|
||||||
"apollo-server-testing": "^2.25.2",
|
"apollo-server-testing": "^2.25.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
@ -39,7 +40,8 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sodium-native": "^3.3.0",
|
"sodium-native": "^3.3.0",
|
||||||
"ts-jest": "^27.0.5",
|
"ts-jest": "^27.0.5",
|
||||||
"type-graphql": "^1.1.1"
|
"type-graphql": "^1.1.1",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.12",
|
"@types/express": "^4.17.12",
|
||||||
|
|||||||
@ -31,6 +31,8 @@ export enum RIGHTS {
|
|||||||
LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS',
|
LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS',
|
||||||
UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION',
|
UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION',
|
||||||
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
||||||
|
COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS',
|
||||||
|
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
|
||||||
// Admin
|
// Admin
|
||||||
SEARCH_USERS = 'SEARCH_USERS',
|
SEARCH_USERS = 'SEARCH_USERS',
|
||||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||||
|
|||||||
@ -28,7 +28,9 @@ export const ROLE_USER = new Role('user', [
|
|||||||
RIGHTS.LIST_CONTRIBUTIONS,
|
RIGHTS.LIST_CONTRIBUTIONS,
|
||||||
RIGHTS.LIST_ALL_CONTRIBUTIONS,
|
RIGHTS.LIST_ALL_CONTRIBUTIONS,
|
||||||
RIGHTS.UPDATE_CONTRIBUTION,
|
RIGHTS.UPDATE_CONTRIBUTION,
|
||||||
|
RIGHTS.SEARCH_ADMIN_USERS,
|
||||||
RIGHTS.LIST_CONTRIBUTION_LINKS,
|
RIGHTS.LIST_CONTRIBUTION_LINKS,
|
||||||
|
RIGHTS.COMMUNITY_STATISTICS,
|
||||||
])
|
])
|
||||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0045-add_denied_type_and_status_to_contributions',
|
DB_VERSION: '0046-adapt_users_table_for_gradidoid',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||||
LOG4JS_CONFIG: 'log4js-config.json',
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
// default log level on production should be info
|
// default log level on production should be info
|
||||||
|
|||||||
25
backend/src/graphql/model/AdminUser.ts
Normal file
25
backend/src/graphql/model/AdminUser.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { User } from '@entity/User'
|
||||||
|
import { Field, Int, ObjectType } from 'type-graphql'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class AdminUser {
|
||||||
|
constructor(user: User) {
|
||||||
|
this.firstName = user.firstName
|
||||||
|
this.lastName = user.lastName
|
||||||
|
}
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
firstName: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class SearchAdminUsersResult {
|
||||||
|
@Field(() => Int)
|
||||||
|
userCount: number
|
||||||
|
|
||||||
|
@Field(() => [AdminUser])
|
||||||
|
userList: AdminUser[]
|
||||||
|
}
|
||||||
26
backend/src/graphql/model/CommunityStatistics.ts
Normal file
26
backend/src/graphql/model/CommunityStatistics.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class CommunityStatistics {
|
||||||
|
@Field(() => Number)
|
||||||
|
totalUsers: number
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
activeUsers: number
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
deletedUsers: number
|
||||||
|
|
||||||
|
@Field(() => Decimal)
|
||||||
|
totalGradidoCreated: Decimal
|
||||||
|
|
||||||
|
@Field(() => Decimal)
|
||||||
|
totalGradidoDecayed: Decimal
|
||||||
|
|
||||||
|
@Field(() => Decimal)
|
||||||
|
totalGradidoAvailable: Decimal
|
||||||
|
|
||||||
|
@Field(() => Decimal)
|
||||||
|
totalGradidoUnbookedDecayed: Decimal
|
||||||
|
}
|
||||||
@ -8,6 +8,8 @@ import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
|||||||
export class User {
|
export class User {
|
||||||
constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) {
|
constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) {
|
||||||
this.id = user.id
|
this.id = user.id
|
||||||
|
this.gradidoID = user.gradidoID
|
||||||
|
this.alias = user.alias
|
||||||
this.email = user.email
|
this.email = user.email
|
||||||
this.firstName = user.firstName
|
this.firstName = user.firstName
|
||||||
this.lastName = user.lastName
|
this.lastName = user.lastName
|
||||||
@ -28,6 +30,12 @@ export class User {
|
|||||||
// `public_key` binary(32) DEFAULT NULL,
|
// `public_key` binary(32) DEFAULT NULL,
|
||||||
// `privkey` binary(80) DEFAULT NULL,
|
// `privkey` binary(80) DEFAULT NULL,
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
gradidoID: string
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
alias: string
|
||||||
|
|
||||||
// TODO privacy issue here
|
// TODO privacy issue here
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
email: string
|
email: string
|
||||||
|
|||||||
77
backend/src/graphql/resolver/StatisticsResolver.ts
Normal file
77
backend/src/graphql/resolver/StatisticsResolver.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { Resolver, Query, Authorized } from 'type-graphql'
|
||||||
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
|
import { CommunityStatistics } from '@model/CommunityStatistics'
|
||||||
|
import { User as DbUser } from '@entity/User'
|
||||||
|
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||||
|
import { getConnection } from '@dbTools/typeorm'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { calculateDecay } from '@/util/decay'
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class StatisticsResolver {
|
||||||
|
@Authorized([RIGHTS.COMMUNITY_STATISTICS])
|
||||||
|
@Query(() => CommunityStatistics)
|
||||||
|
async communityStatistics(): Promise<CommunityStatistics> {
|
||||||
|
const allUsers = await DbUser.find({ withDeleted: true })
|
||||||
|
|
||||||
|
let totalUsers = 0
|
||||||
|
let activeUsers = 0
|
||||||
|
let deletedUsers = 0
|
||||||
|
|
||||||
|
let totalGradidoAvailable: Decimal = new Decimal(0)
|
||||||
|
let totalGradidoUnbookedDecayed: Decimal = new Decimal(0)
|
||||||
|
|
||||||
|
const receivedCallDate = new Date()
|
||||||
|
|
||||||
|
for (let i = 0; i < allUsers.length; i++) {
|
||||||
|
if (allUsers[i].deletedAt) {
|
||||||
|
deletedUsers++
|
||||||
|
} else {
|
||||||
|
totalUsers++
|
||||||
|
const lastTransaction = await DbTransaction.findOne({
|
||||||
|
where: { userId: allUsers[i].id },
|
||||||
|
order: { balanceDate: 'DESC' },
|
||||||
|
})
|
||||||
|
if (lastTransaction) {
|
||||||
|
activeUsers++
|
||||||
|
const decay = calculateDecay(
|
||||||
|
lastTransaction.balance,
|
||||||
|
lastTransaction.balanceDate,
|
||||||
|
receivedCallDate,
|
||||||
|
)
|
||||||
|
if (decay) {
|
||||||
|
totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString())
|
||||||
|
totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
|
await queryRunner.connect()
|
||||||
|
|
||||||
|
const { totalGradidoCreated } = await queryRunner.manager
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('SUM(transaction.amount) AS totalGradidoCreated')
|
||||||
|
.from(DbTransaction, 'transaction')
|
||||||
|
.where('transaction.typeId = 1')
|
||||||
|
.getRawOne()
|
||||||
|
|
||||||
|
const { totalGradidoDecayed } = await queryRunner.manager
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('SUM(transaction.decay) AS totalGradidoDecayed')
|
||||||
|
.from(DbTransaction, 'transaction')
|
||||||
|
.where('transaction.decay IS NOT NULL')
|
||||||
|
.getRawOne()
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalUsers,
|
||||||
|
activeUsers,
|
||||||
|
deletedUsers,
|
||||||
|
totalGradidoCreated,
|
||||||
|
totalGradidoDecayed,
|
||||||
|
totalGradidoAvailable,
|
||||||
|
totalGradidoUnbookedDecayed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } fro
|
|||||||
import { userFactory } from '@/seeds/factory/user'
|
import { userFactory } from '@/seeds/factory/user'
|
||||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||||
import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations'
|
import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations'
|
||||||
import { login, logout, verifyLogin, queryOptIn } from '@/seeds/graphql/queries'
|
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
@ -20,6 +20,8 @@ import { ContributionLink } from '@model/ContributionLink'
|
|||||||
// import { TransactionLink } from '@entity/TransactionLink'
|
// import { TransactionLink } from '@entity/TransactionLink'
|
||||||
|
|
||||||
import { logger } from '@test/testSetup'
|
import { logger } from '@test/testSetup'
|
||||||
|
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||||
|
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||||
|
|
||||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
|
|
||||||
@ -111,6 +113,8 @@ describe('UserResolver', () => {
|
|||||||
expect(user).toEqual([
|
expect(user).toEqual([
|
||||||
{
|
{
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
|
gradidoID: expect.any(String),
|
||||||
|
alias: null,
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
@ -129,6 +133,10 @@ describe('UserResolver', () => {
|
|||||||
contributionLinkId: null,
|
contributionLinkId: null,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
const valUUID = validateUUID(user[0].gradidoID)
|
||||||
|
const verUUID = versionUUID(user[0].gradidoID)
|
||||||
|
expect(valUUID).toEqual(true)
|
||||||
|
expect(verUUID).toEqual(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates an email optin', () => {
|
it('creates an email optin', () => {
|
||||||
@ -871,6 +879,51 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('searchAdminUsers', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
resetToken()
|
||||||
|
await expect(mutate({ mutation: searchAdminUsers })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
|
await userFactory(testEnv, peterLustig)
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: {
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
password: 'Aa12345_',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds peter@lustig.de', async () => {
|
||||||
|
await expect(mutate({ mutation: searchAdminUsers })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
searchAdminUsers: {
|
||||||
|
userCount: 1,
|
||||||
|
userList: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Lustig',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('printTimeDuration', () => {
|
describe('printTimeDuration', () => {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { backendLogger as logger } from '@/server/logger'
|
|||||||
|
|
||||||
import { Context, getUser } from '@/server/context'
|
import { Context, getUser } from '@/server/context'
|
||||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||||
import { getConnection } from '@dbTools/typeorm'
|
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
@ -32,6 +32,11 @@ import {
|
|||||||
EventSendConfirmationEmail,
|
EventSendConfirmationEmail,
|
||||||
} from '@/event/Event'
|
} from '@/event/Event'
|
||||||
import { getUserCreation } from './util/creations'
|
import { getUserCreation } from './util/creations'
|
||||||
|
import { UserRepository } from '@/typeorm/repository/User'
|
||||||
|
import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||||
|
import Paginated from '@arg/Paginated'
|
||||||
|
import { Order } from '@enum/Order'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const sodium = require('sodium-native')
|
const sodium = require('sodium-native')
|
||||||
@ -227,6 +232,19 @@ export const activationLink = (optInCode: LoginEmailOptIn): string => {
|
|||||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
|
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newGradidoID = async (): Promise<string> => {
|
||||||
|
let gradidoId: string
|
||||||
|
let countIds: number
|
||||||
|
do {
|
||||||
|
gradidoId = uuidv4()
|
||||||
|
countIds = await DbUser.count({ where: { gradidoID: gradidoId } })
|
||||||
|
if (countIds > 0) {
|
||||||
|
logger.info('Gradido-ID creation conflict...')
|
||||||
|
}
|
||||||
|
} while (countIds > 0)
|
||||||
|
return gradidoId
|
||||||
|
}
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
@Authorized([RIGHTS.VERIFY_LOGIN])
|
@Authorized([RIGHTS.VERIFY_LOGIN])
|
||||||
@ -347,11 +365,13 @@ export class UserResolver {
|
|||||||
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
||||||
|
|
||||||
if (userFound) {
|
if (userFound) {
|
||||||
logger.info('User already exists with this email=' + email)
|
// ATTENTION: this logger-message will be exactly expected during tests
|
||||||
|
logger.info(`User already exists with this email=${email}`)
|
||||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||||
|
|
||||||
const user = new User(communityDbUser)
|
const user = new User(communityDbUser)
|
||||||
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
||||||
|
user.gradidoID = uuidv4()
|
||||||
user.email = email
|
user.email = email
|
||||||
user.firstName = firstName
|
user.firstName = firstName
|
||||||
user.lastName = lastName
|
user.lastName = lastName
|
||||||
@ -381,11 +401,13 @@ export class UserResolver {
|
|||||||
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||||
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||||
const emailHash = getEmailHash(email)
|
const emailHash = getEmailHash(email)
|
||||||
|
const gradidoID = await newGradidoID()
|
||||||
|
|
||||||
const eventRegister = new EventRegister()
|
const eventRegister = new EventRegister()
|
||||||
const eventRedeemRegister = new EventRedeemRegister()
|
const eventRedeemRegister = new EventRedeemRegister()
|
||||||
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
||||||
const dbUser = new DbUser()
|
const dbUser = new DbUser()
|
||||||
|
dbUser.gradidoID = gradidoID
|
||||||
dbUser.email = email
|
dbUser.email = email
|
||||||
dbUser.firstName = firstName
|
dbUser.firstName = firstName
|
||||||
dbUser.lastName = lastName
|
dbUser.lastName = lastName
|
||||||
@ -734,6 +756,36 @@ export class UserResolver {
|
|||||||
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
||||||
return elopageBuys
|
return elopageBuys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.SEARCH_ADMIN_USERS])
|
||||||
|
@Query(() => SearchAdminUsersResult)
|
||||||
|
async searchAdminUsers(
|
||||||
|
@Args()
|
||||||
|
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
||||||
|
): Promise<SearchAdminUsersResult> {
|
||||||
|
const userRepository = getCustomRepository(UserRepository)
|
||||||
|
|
||||||
|
const [users, count] = await userRepository.findAndCount({
|
||||||
|
where: {
|
||||||
|
isAdmin: Not(IsNull()),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: order,
|
||||||
|
},
|
||||||
|
skip: (currentPage - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
userCount: count,
|
||||||
|
userList: users.map((user) => {
|
||||||
|
return {
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
||||||
|
|||||||
@ -280,3 +280,15 @@ export const listContributionLinks = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const searchAdminUsers = gql`
|
||||||
|
query {
|
||||||
|
searchAdminUsers {
|
||||||
|
userCount
|
||||||
|
userList {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { User } from '@model/User'
|
|||||||
|
|
||||||
const communityDbUser: dbUser = {
|
const communityDbUser: dbUser = {
|
||||||
id: -1,
|
id: -1,
|
||||||
|
gradidoID: '11111111-2222-4333-4444-55555555',
|
||||||
|
alias: '',
|
||||||
email: 'support@gradido.net',
|
email: 'support@gradido.net',
|
||||||
firstName: 'Gradido',
|
firstName: 'Gradido',
|
||||||
lastName: 'Akademie',
|
lastName: 'Akademie',
|
||||||
|
|||||||
@ -1000,6 +1000,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||||
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
||||||
|
|
||||||
|
"@types/uuid@^8.3.4":
|
||||||
|
version "8.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
||||||
|
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
|
||||||
|
|
||||||
"@types/validator@^13.1.3":
|
"@types/validator@^13.1.3":
|
||||||
version "13.6.3"
|
version "13.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.6.3.tgz#31ca2e997bf13a0fffca30a25747d5b9f7dbb7de"
|
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.6.3.tgz#31ca2e997bf13a0fffca30a25747d5b9f7dbb7de"
|
||||||
@ -5437,7 +5442,7 @@ uuid@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||||
|
|
||||||
uuid@^8.0.0:
|
uuid@^8.0.0, uuid@^8.3.2:
|
||||||
version "8.3.2"
|
version "8.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|||||||
111
database/entity/0046-adapt_users_table_for_gradidoid/User.ts
Normal file
111
database/entity/0046-adapt_users_table_for_gradidoid/User.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
DeleteDateColumn,
|
||||||
|
OneToMany,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { Contribution } from '../Contribution'
|
||||||
|
|
||||||
|
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||||
|
export class User extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'gradido_id',
|
||||||
|
length: 36,
|
||||||
|
nullable: false,
|
||||||
|
unique: true,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
gradidoID: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'alias',
|
||||||
|
length: 20,
|
||||||
|
nullable: true,
|
||||||
|
unique: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
alias: string
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
pubKey: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||||
|
privKey: Buffer
|
||||||
|
|
||||||
|
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||||
|
password: BigInt
|
||||||
|
|
||||||
|
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
emailHash: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||||
|
emailChecked: boolean
|
||||||
|
|
||||||
|
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||||
|
language: string
|
||||||
|
|
||||||
|
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
|
||||||
|
isAdmin: Date | null
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'text',
|
||||||
|
name: 'passphrase',
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
passphrase: string
|
||||||
|
|
||||||
|
@OneToMany(() => Contribution, (contribution) => contribution.user)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
contributions?: Contribution[]
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export { User } from './0040-add_contribution_link_id_to_user/User'
|
export { User } from './0046-adapt_users_table_for_gradidoid/User'
|
||||||
|
|||||||
44
database/migrations/0046-adapt_users_table_for_gradidoid.ts
Normal file
44
database/migrations/0046-adapt_users_table_for_gradidoid.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/* MIGRATION TO ADD GRADIDO_ID
|
||||||
|
*
|
||||||
|
* This migration adds new columns to the table `users` and creates the
|
||||||
|
* new table `user_contacts`
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
// First add gradido_id as nullable column without Default
|
||||||
|
await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` CHAR(36) NULL AFTER `id`;')
|
||||||
|
|
||||||
|
// Second update gradido_id with ensured unique uuidv4
|
||||||
|
const usersToUpdate = await queryFn('SELECT `id`, `gradido_id` FROM `users`') // WHERE 'u.gradido_id' is null`,)
|
||||||
|
for (const id in usersToUpdate) {
|
||||||
|
const user = usersToUpdate[id]
|
||||||
|
let gradidoId = null
|
||||||
|
let countIds = null
|
||||||
|
do {
|
||||||
|
gradidoId = uuidv4()
|
||||||
|
countIds = await queryFn(
|
||||||
|
`SELECT COUNT(*) FROM \`users\` WHERE \`gradido_id\` = "${gradidoId}"`,
|
||||||
|
)
|
||||||
|
} while (countIds[0] > 0)
|
||||||
|
await queryFn(
|
||||||
|
`UPDATE \`users\` SET \`gradido_id\` = "${gradidoId}" WHERE \`id\` = "${user.id}"`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// third modify gradido_id to not nullable and unique
|
||||||
|
await queryFn('ALTER TABLE `users` MODIFY COLUMN `gradido_id` CHAR(36) NOT NULL UNIQUE;')
|
||||||
|
|
||||||
|
await queryFn(
|
||||||
|
'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn('ALTER TABLE users DROP COLUMN gradido_id;')
|
||||||
|
await queryFn('ALTER TABLE users DROP COLUMN alias;')
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@
|
|||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"decimal.js-light": "^2.5.1",
|
"decimal.js-light": "^2.5.1",
|
||||||
@ -44,6 +45,7 @@
|
|||||||
"mysql2": "^2.3.0",
|
"mysql2": "^2.3.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"ts-mysql-migrate": "^1.0.2",
|
"ts-mysql-migrate": "^1.0.2",
|
||||||
"typeorm": "^0.2.38"
|
"typeorm": "^0.2.38",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -137,6 +137,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.3.tgz#7a8f2838603ea314d1d22bb3171d899e15c57bd5"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.3.tgz#7a8f2838603ea314d1d22bb3171d899e15c57bd5"
|
||||||
integrity sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==
|
integrity sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==
|
||||||
|
|
||||||
|
"@types/uuid@^8.3.4":
|
||||||
|
version "8.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
||||||
|
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
|
||||||
|
|
||||||
"@types/zen-observable@0.8.3":
|
"@types/zen-observable@0.8.3":
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3"
|
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3"
|
||||||
@ -2088,6 +2093,11 @@ util-deprecate@~1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||||
|
|
||||||
|
uuid@^8.3.2:
|
||||||
|
version "8.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
||||||
v8-compile-cache@^2.0.3:
|
v8-compile-cache@^2.0.3:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user