Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"eslint:fix": "npm run eslint -- --fix",
"posttest": "echo 'Finished running all tests.'",
"migrate": "node ./dist/migrate",
"backfill:organization-species": "node ./dist/backfillOrganizationSpecies",
"prestart": "npm run clean && npm run build",
"start": "NODE_ENV=development node .",
"start:debug": "NODE_ENV=development DEBUG=loopback:*,express:* node --inspect .",
Expand Down
95 changes: 95 additions & 0 deletions src/backfillOrganizationSpecies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { TreetrackerAdminApiApplication } from './application';
import {
OrganizationRepository,
TreesRepository,
OrganizationSpeciesRepository,
} from './repositories';

export async function backfillOrganizationSpecies(
args: string[],
): Promise<void> {
const dryRun = args.includes('--dry-run');
console.log(
'Backfilling organization_species from historical tree usage (dry-run: %s)',
dryRun,
);

const app = new TreetrackerAdminApiApplication();
await app.boot();

const organizationRepository = await app.getRepository(
OrganizationRepository,
);
const treesRepository = await app.getRepository(TreesRepository);
const organizationSpeciesRepository = await app.getRepository(
OrganizationSpeciesRepository,
);
const organizations = await organizationRepository.find();
console.log('Found %d organizations', organizations.length);
const now = new Date();

for (const organization of organizations) {
const organizationId = organization.id as number;

const organizationWhereClause =
await treesRepository.getOrganizationWhereClause(organizationId);

const trees = await treesRepository.find({
where: {
and: [organizationWhereClause, { speciesId: { neq: null } }],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
fields: {
speciesId: true,
},
});
const usedSpeciesIds = [
...new Set(
trees
.map((tree) => tree.speciesId as number)
.filter((speciesId) => speciesId != null),
),
];
if (usedSpeciesIds.length === 0) {
continue;
}
const existingMappings = await organizationSpeciesRepository.find({
where: {
organizationId,
},
});
const alreadyMappedSpeciesIds = new Set(
existingMappings.map((mapping) => mapping.speciesId),
);
const toInsert = usedSpeciesIds
.filter((speciesId) => !alreadyMappedSpeciesIds.has(speciesId))
.map((speciesId) => ({
organizationId,
speciesId,
isActive: true,
timeCreated: now,
timeUpdated: now,
}));
if (toInsert.length === 0) {
continue;
}
console.log(
'Organization %d: %d species used historically, %d new mapping(s) to insert',
organizationId,
usedSpeciesIds.length,
toInsert.length,
);
if (!dryRun) {
await organizationSpeciesRepository.createAll(toInsert);
}
}
process.exit(0);
}

backfillOrganizationSpecies(process.argv).catch((err) => {
console.error(
'Failed to backfill organization_species from historical tree usage',
err,
);
process.exit(1);
});
1 change: 1 addition & 0 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './tag.controller';
export * from './treeTag.controller';
export * from './treesTreeTag.controller';
export * from './organization.controller';
export * from './organizationSpecies.controller';
146 changes: 146 additions & 0 deletions src/controllers/organizationSpecies.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { inject } from '@loopback/context';
import { repository, Filter } from '@loopback/repository';
import {
Request,
RestBindings,
get,
post,
param,
getFilterSchemaFor,
HttpErrors,
} from '@loopback/rest';
import {
OrganizationSpeciesRepository,
SpeciesRepository,
} from '../repositories';
import { Species, OrganizationSpecies } from '../models';
const WRITE_ALLOWED_POLICIES = ['super_permission', 'manage_org_species'];

export class OrganizationSpeciesController {
constructor(
@repository(OrganizationSpeciesRepository)
public organizationSpeciesRepository: OrganizationSpeciesRepository,
@repository(SpeciesRepository)
public speciesRepository: SpeciesRepository,
@inject(RestBindings.Http.REQUEST)
private request: Request,
) {}

@get('/organization/{organizationId}/species', {
responses: {
'200': {
description: 'Array of Species model instances',
content: {
'application/json': {
schema: { type: 'array', items: { 'x-ts-type': Species } },
},
},
},
},
})
async findScopedSpecies(
@param.path.number('organizationId') organizationId: number,
@param.query.object('filter', getFilterSchemaFor(Species))
filter?: Filter<Species>,
): Promise<Species[]> {
const links = await this.organizationSpeciesRepository.find({
where: { organizationId, isActive: true },
});
const speciesIds = links.map((link) => link.speciesId);
if (speciesIds.length === 0) {
return [];
}
return await this.speciesRepository.find({
where: { id: { inq: speciesIds }, active: true },
order: filter?.order ?? ['name ASC'],
});
}
// Activate
@post('/organization/{organizationId}/species/{speciesId}/activate', {
responses: {
'200': {
description: 'Activate a species for an organization',
content: {
'application/json': { schema: { 'x-ts-type': OrganizationSpecies } },
},
},
},
})
async activate(
@param.path.number('organizationId') organizationId: number,
@param.path.number('speciesId') speciesId: number,
): Promise<OrganizationSpecies> {
this.assertWriteAllowed();
// Catalog check - refuse with 422
const species = await this.speciesRepository.findOne({
where: { id: speciesId },
});
if (!species || !species.active) {
throw new HttpErrors.UnprocessableEntity('species is not active');
}
const existing = await this.organizationSpeciesRepository.findOne({
where: { organizationId, speciesId },
});
// Write into the table
if (existing) {
const now = new Date();
await this.organizationSpeciesRepository.updateById(existing.id, {
isActive: true,
timeUpdated: now,
});
existing.isActive = true;
existing.timeUpdated = now;
return existing;
}
return await this.organizationSpeciesRepository.create({
organizationId,
speciesId,
isActive: true,
timeCreated: new Date(),
timeUpdated: new Date(),
});
}
// Deactivate
@post('/organization/{organizationId}/species/{speciesId}/deactivate', {
responses: {
'204': {
description: 'Deactivate a species for an organization',
},
},
})
async deactivate(
@param.path.number('organizationId') organizationId: number,
@param.path.number('speciesId') speciesId: number,
): Promise<void> {
this.assertWriteAllowed();
const existing = await this.organizationSpeciesRepository.findOne({
where: { organizationId, speciesId },
});
if (existing) {
await this.organizationSpeciesRepository.updateById(existing.id, {
isActive: false,
timeUpdated: new Date(),
});
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
requestHasUser(request: Request): request is Request & { user: any } {
return 'user' in request;
}

private assertWriteAllowed(): void {
let isAllowed = false;
if (this.requestHasUser(this.request)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const userPolicies: any[] = this.request.user.policy.policies;
isAllowed = userPolicies.some((userPolicy) =>
WRITE_ALLOWED_POLICIES.some(
(allowedPolicy) => allowedPolicy === userPolicy.name,
),
);
}
if (!isAllowed) {
throw new HttpErrors.Unauthorized('No permission');
}
}
}
26 changes: 26 additions & 0 deletions src/js/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const POLICIES = {
APPROVE_TREE: 'approve_tree',
LIST_PLANTER: 'list_planter',
MANAGE_PLANTER: 'manage_planter',
LIST_SPECIES: 'list_species',
MANAGE_ORG_SPECIES: 'manage_org_species',
};

helper.needRoleUpdate = function (update_userSession, userSession) {
Expand Down Expand Up @@ -581,6 +583,30 @@ const isAuth = async (req, res, next) => {
return next();
}

matcher = url.match(/\/api\/(organization\/(\d+)\/)?species.*/);
if (matcher) {
const requestedOrgId = matcher.length > 1 && parseInt(matcher[2], 10);
if (
helper.hasPermission(
policies,
organization,
[
POLICIES.SUPER_PERMISSION,
POLICIES.LIST_SPECIES,
POLICIES.MANAGE_ORG_SPECIES,
],
requestedOrgId,
)
) {
return next();
}

res.status(401).json({
error: new Error('No permission'),
});
return;
}

matcher = url.match(/\/api\/(organization\/(\d+)\/)?trees.*/);
if (matcher) {
const requestedOrgId = matcher.length > 1 && parseInt(matcher[2], 10);
Expand Down
Loading