-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: move user export to direct download (#3882)
- Loading branch information
1 parent
cf398c0
commit 74a7446
Showing
16 changed files
with
681 additions
and
396 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,7 @@ import { | |
simplifiedDCMap, | ||
} from './seed-helpers/map-layer-factory'; | ||
import { ValidationMethod } from '../src/enums/multiselect-questions/validation-method-enum'; | ||
import { randomNoun } from './seed-helpers/word-generator'; | ||
|
||
export const stagingSeed = async ( | ||
prismaClient: PrismaClient, | ||
|
@@ -45,7 +46,7 @@ export const stagingSeed = async ( | |
}); | ||
// add another jurisdiction | ||
const additionalJurisdiction = await prismaClient.jurisdictions.create({ | ||
data: jurisdictionFactory(), | ||
data: jurisdictionFactory(randomNoun()), | ||
}); | ||
// create admin user | ||
await prismaClient.userAccounts.create({ | ||
|
@@ -69,7 +70,7 @@ export const stagingSeed = async ( | |
}); | ||
await prismaClient.userAccounts.create({ | ||
data: await userFactory({ | ||
roles: { isJurisdictionalAdmin: true }, | ||
roles: { isAdmin: true }, | ||
email: '[email protected]', | ||
confirmedAt: new Date(), | ||
jurisdictionIds: [jurisdiction.id], | ||
|
@@ -78,7 +79,7 @@ export const stagingSeed = async ( | |
}); | ||
await prismaClient.userAccounts.create({ | ||
data: await userFactory({ | ||
roles: { isJurisdictionalAdmin: true }, | ||
roles: { isAdmin: true }, | ||
email: '[email protected]', | ||
confirmedAt: new Date(), | ||
jurisdictionIds: [jurisdiction.id], | ||
|
@@ -882,9 +883,25 @@ export const stagingSeed = async ( | |
applications: value.applications, | ||
afsLastRunSetInPast: true, | ||
}); | ||
await prismaClient.listings.create({ | ||
const savedListing = await prismaClient.listings.create({ | ||
data: listing, | ||
}); | ||
if (index === 0) { | ||
await prismaClient.userAccounts.create({ | ||
data: await userFactory({ | ||
roles: { | ||
isAdmin: false, | ||
isPartner: true, | ||
isJurisdictionalAdmin: false, | ||
}, | ||
email: '[email protected]', | ||
confirmedAt: new Date(), | ||
jurisdictionIds: [jurisdiction.id, additionalJurisdiction.id], | ||
acceptedTerms: true, | ||
listings: [savedListing.id], | ||
}), | ||
}); | ||
} | ||
}, | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,16 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { Logger, Module } from '@nestjs/common'; | ||
import { ConfigService } from '@nestjs/config'; | ||
import { UserController } from '../controllers/user.controller'; | ||
import { UserService } from '../services/user.service'; | ||
import { PrismaModule } from './prisma.module'; | ||
import { EmailModule } from './email.module'; | ||
import { PermissionModule } from './permission.module'; | ||
import { UserCsvExporterService } from '../services/user-csv-export.service'; | ||
|
||
@Module({ | ||
imports: [PrismaModule, EmailModule, PermissionModule], | ||
controllers: [UserController], | ||
providers: [UserService, ConfigService], | ||
providers: [Logger, UserService, ConfigService, UserCsvExporterService], | ||
exports: [UserService], | ||
}) | ||
export class UserModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
import { ForbiddenException, Injectable, StreamableFile } from '@nestjs/common'; | ||
import dayjs from 'dayjs'; | ||
import { Request as ExpressRequest, Response } from 'express'; | ||
import fs, { createReadStream } from 'fs'; | ||
import { join } from 'path'; | ||
import { | ||
CsvExporterServiceInterface, | ||
CsvHeader, | ||
} from '../types/CsvExportInterface'; | ||
import { PrismaService } from './prisma.service'; | ||
import { User } from '../dtos/users/user.dto'; | ||
import { UserRole } from '../dtos/users/user-role.dto'; | ||
import { mapTo } from '../utilities/mapTo'; | ||
import { IdDTO } from '../dtos/shared/id.dto'; | ||
import { buildWhereClause } from '../utilities/build-user-where'; | ||
|
||
@Injectable() | ||
export class UserCsvExporterService implements CsvExporterServiceInterface { | ||
constructor(private prisma: PrismaService) {} | ||
/** | ||
* | ||
* @param queryParams | ||
* @param req | ||
* @returns a promise containing a streamable file | ||
*/ | ||
async exportFile<QueryParams>( | ||
req: ExpressRequest, | ||
res: Response, | ||
queryParams?: QueryParams, | ||
): Promise<StreamableFile> { | ||
const user = mapTo(User, req['user']); | ||
await this.authorizeCSVExport(mapTo(User, req['user'])); | ||
const filename = join( | ||
process.cwd(), | ||
`src/temp/users-${user.id}-${new Date().getTime()}.csv`, | ||
); | ||
await this.createCsv(filename, queryParams, { user: user }); | ||
const file = createReadStream(filename); | ||
return new StreamableFile(file); | ||
} | ||
|
||
/** | ||
* | ||
* @param filename | ||
* @param queryParams | ||
* @returns a promise with SuccessDTO | ||
*/ | ||
async createCsv<QueryParams>( | ||
filename: string, | ||
queryParams: QueryParams, | ||
optionalParams: { user: User }, | ||
): Promise<void> { | ||
const where = buildWhereClause( | ||
{ filter: [{ isPortalUser: true }] }, | ||
optionalParams.user, | ||
); | ||
const users = await this.prisma.userAccounts.findMany({ | ||
where: where, | ||
include: { | ||
userRoles: true, | ||
listings: true, | ||
}, | ||
}); | ||
const csvHeaders = await this.getCsvHeaders(); | ||
return new Promise((resolve, reject) => { | ||
// create stream | ||
const writableStream = fs.createWriteStream(`${filename}`); | ||
writableStream | ||
.on('error', (err) => { | ||
console.log('csv writestream error'); | ||
console.log(err); | ||
reject(err); | ||
}) | ||
.on('close', () => { | ||
resolve(); | ||
}) | ||
.on('open', () => { | ||
writableStream.write( | ||
csvHeaders | ||
.map((header) => `"${header.label.replace(/"/g, `""`)}"`) | ||
.join(',') + '\n', | ||
); | ||
|
||
// now loop over users and write them to file | ||
users.forEach((user) => { | ||
let row = ''; | ||
csvHeaders.forEach((header, index) => { | ||
let value = header.path.split('.').reduce((acc, curr) => { | ||
// handles working with arrays | ||
if (!isNaN(Number(curr))) { | ||
const index = Number(curr); | ||
return acc[index]; | ||
} | ||
|
||
if (acc === null || acc === undefined) { | ||
return ''; | ||
} | ||
return acc[curr]; | ||
}, user); | ||
value = value === undefined ? '' : value === null ? '' : value; | ||
|
||
if (header.format) { | ||
value = header.format(value, user); | ||
} | ||
|
||
row += value ? `"${value.toString().replace(/"/g, `""`)}"` : ''; | ||
if (index < csvHeaders.length - 1) { | ||
row += ','; | ||
} | ||
}); | ||
|
||
try { | ||
writableStream.write(row + '\n'); | ||
} catch (e) { | ||
console.log('writeStream write error = ', e); | ||
writableStream.once('drain', () => { | ||
writableStream.write(row + '\n'); | ||
}); | ||
} | ||
}); | ||
|
||
writableStream.end(); | ||
}); | ||
}); | ||
} | ||
|
||
async getCsvHeaders(): Promise<CsvHeader[]> { | ||
const headers: CsvHeader[] = [ | ||
{ | ||
path: 'firstName', | ||
label: 'First Name', | ||
}, | ||
{ | ||
path: 'lastName', | ||
label: 'Last Name', | ||
}, | ||
{ | ||
path: 'email', | ||
label: 'Email', | ||
}, | ||
{ | ||
path: 'userRoles', | ||
label: 'Role', | ||
format: (val: UserRole): string => { | ||
const roles: string[] = []; | ||
if (val?.isAdmin) { | ||
roles.push('Administrator'); | ||
} | ||
if (val?.isPartner) { | ||
roles.push('Partner'); | ||
} | ||
if (val?.isJurisdictionalAdmin) { | ||
roles.push('Jurisdictional Admin'); | ||
} | ||
return roles.join(', '); | ||
}, | ||
}, | ||
{ | ||
path: 'createdAt', | ||
label: 'Date Created', | ||
format: (val: string): string => { | ||
return dayjs(val).format('MM-DD-YYYY'); | ||
}, | ||
}, | ||
{ | ||
path: 'confirmedAt', | ||
label: 'Status', | ||
format: (val: string): string => (val ? 'Confirmed' : 'Unconfirmed'), | ||
}, | ||
{ | ||
path: 'listings', | ||
label: 'Listing Names', | ||
format: (val: IdDTO[]): string => { | ||
return val?.length | ||
? val?.map((listing) => listing.name).join(', ') | ||
: ''; | ||
}, | ||
}, | ||
{ | ||
path: 'listings', | ||
label: 'Listing Ids', | ||
format: (val: IdDTO[]): string => { | ||
return val?.length | ||
? val?.map((listing) => listing.id).join(', ') | ||
: ''; | ||
}, | ||
}, | ||
{ | ||
path: 'lastLoginAt', | ||
label: 'Last Logged In', | ||
format: (val: string): string => { | ||
return dayjs(val).format('MM-DD-YYYY'); | ||
}, | ||
}, | ||
]; | ||
|
||
return headers; | ||
} | ||
|
||
async authorizeCSVExport(user?: User): Promise<void> { | ||
if ( | ||
user && | ||
(user.userRoles?.isAdmin || user.userRoles?.isJurisdictionalAdmin) | ||
) { | ||
return; | ||
} else { | ||
throw new ForbiddenException(); | ||
} | ||
} | ||
} |
Oops, something went wrong.