Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace "User" role with "Partner" role #1628

Closed
Show file tree
Hide file tree
Changes from 4 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
8 changes: 8 additions & 0 deletions backend/core/src/auth/dto/user-roles.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { OmitType } from "@nestjs/swagger"
import { UserRoles } from "../entities/user-roles.entity"

export class UserRolesDto extends OmitType(UserRoles, ["isPartner", "isAdmin"] as const) {}

export class UserRolesCreateDto extends OmitType(UserRolesDto, [] as const) {}

export class UserRolesUpdateDto extends OmitType(UserRolesDto, [] as const) {}
6 changes: 1 addition & 5 deletions backend/core/src/auth/dto/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import { Match } from "../../shared/decorators/match.decorator"
import { passwordRegex } from "../../shared/password-regex"

export class UserDto extends OmitType(User, [
"isAdmin",
"leasingAgentInListings",
"passwordHash",
"resetToken",
"confirmationToken",
"roles",
] as const) {
@Expose()
@IsOptional()
Expand All @@ -35,10 +35,8 @@ export class UserDto extends OmitType(User, [
}

export class UserBasicDto extends OmitType(User, [
"isAdmin",
"leasingAgentInListings",
"passwordHash",
"roles",
"confirmationToken",
"resetToken",
] as const) {}
Expand All @@ -65,7 +63,6 @@ export class UserCreateDto extends OmitType(UserDto, [
"createdAt",
"updatedAt",
"leasingAgentInListings",
"roles",
] as const) {
@Expose()
@IsString({ groups: [ValidationsGroupsEnum.default] })
Expand Down Expand Up @@ -98,7 +95,6 @@ export class UserUpdateDto extends OmitType(UserDto, [
"createdAt",
"updatedAt",
"leasingAgentInListings",
"roles",
] as const) {
@Expose()
@IsOptional({ groups: [ValidationsGroupsEnum.default] })
Expand Down
15 changes: 15 additions & 0 deletions backend/core/src/auth/entities/user-roles.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Column, Entity, JoinColumn, OneToOne } from "typeorm"
import { User } from "./user.entity"

@Entity({ name: "user_roles" })
export class UserRoles {
@OneToOne(() => User, (user) => user.roles, { primary: true })
@JoinColumn()
user: User

@Column("boolean", { default: false })
isAdmin: boolean

@Column("boolean", { default: false })
isPartner: boolean
}
25 changes: 8 additions & 17 deletions backend/core/src/auth/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Entity,
Index,
ManyToMany,
OneToOne,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
Expand All @@ -14,7 +15,7 @@ import { IsDate, IsEmail, IsEnum, IsOptional, IsString, IsUUID, MaxLength } from
import { ValidationsGroupsEnum } from "../../shared/types/validations-groups-enum"
import { ApiProperty } from "@nestjs/swagger"
import { Language } from "../../shared/types/language-enum"
import { UserRole } from "../enum/user-role-enum"
import { UserRoles } from "./user-roles.entity"

@Entity({ name: "user_accounts" })
@Unique(["email"])
Expand Down Expand Up @@ -86,22 +87,12 @@ export class User {
@ManyToMany(() => Listing, (listing) => listing.leasingAgents, { nullable: true })
leasingAgentInListings?: Listing[] | null

@Column("boolean", { default: false })
isAdmin: boolean

/**
* Array of roles this user can become. Logic is simple right now, but in theory this will expand to take into
* account membership in a domain (company-level or admin area level for example).
*
* In that case, this logic will likely be based on joined entities (another table/entity that keeps track of
* group membership, for example), and these relations will need to be loaded in order for the list of roles to
* work properly.
*/
@Expose()
@ApiProperty({ enum: UserRole, enumName: "UserRole", isArray: true })
get roles(): UserRole[] {
return [UserRole.user, ...(this.isAdmin ? [UserRole.admin] : [])]
}
@OneToOne(() => UserRoles, (roles) => roles.user, {
eager: true,
cascade: true,
onDelete: "CASCADE",
})
roles: UserRoles

@Column({ enum: Language, nullable: true })
@Expose()
Expand Down
2 changes: 1 addition & 1 deletion backend/core/src/auth/enum/user-role-enum.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum UserRole {
user = "user",
partner = "partner",
admin = "admin",
}
8 changes: 7 additions & 1 deletion backend/core/src/auth/services/authz.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { newEnforcer } from "casbin"
import path from "path"
import { User } from "../entities/user.entity"
import { Listing } from "../../listings/entities/listing.entity"
import { UserRole } from "../enum/user-role-enum"

export enum authzActions {
create = "create",
Expand Down Expand Up @@ -39,7 +40,12 @@ export class AuthzService {

// Get User roles and add them to our enforcer
if (user) {
await Promise.all(user.roles.map((r) => e.addRoleForUser(user.id, r)))
if (user.roles?.isAdmin) {
await e.addRoleForUser(user.id, UserRole.admin)
}
if (user.roles.isPartner) {
await e.addRoleForUser(user.id, UserRole.partner)
}
}

if (user) {
Expand Down
23 changes: 23 additions & 0 deletions backend/core/src/migration/1628276143069-partner-user-roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class partnerUserRoles1628276143069 implements MigrationInterface {
name = 'partnerUserRoles1628276143069'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "user_roles" ("is_admin" boolean NOT NULL DEFAULT false, "is_partner" boolean NOT NULL DEFAULT false, "user_id" uuid NOT NULL, CONSTRAINT "REL_87b8888186ca9769c960e92687" UNIQUE ("user_id"), CONSTRAINT "PK_87b8888186ca9769c960e926870" PRIMARY KEY ("user_id"))`);
await queryRunner.query(`ALTER TABLE "user_roles" ADD CONSTRAINT "FK_87b8888186ca9769c960e926870" FOREIGN KEY ("user_id") REFERENCES "user_accounts"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`INSERT INTO "user_roles" ("user_id") SELECT "id" FROM "user_accounts"`)
// Initially, all existing users have partner access. This should be revoked over time.
abbiefarr marked this conversation as resolved.
Show resolved Hide resolved
await queryRunner.query(`UPDATE "user_roles" SET "is_partner" = TRUE`)
await queryRunner.query(`UPDATE "user_roles" SET "is_admin" = "user_accounts"."is_admin" FROM "user_accounts" WHERE "user_accounts"."id" = "user_roles"."user_id"`)
await queryRunner.query(`ALTER TABLE "user_accounts" DROP COLUMN "is_admin"`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_accounts" ADD "is_admin" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`UPDATE "user_accounts" SET "is_admin" = "user_roles"."is_admin" FROM "user_roles" WHERE "user_roles"."user_id" = "user_accounts"."id"`)
await queryRunner.query(`ALTER TABLE "user_roles" DROP CONSTRAINT "FK_87b8888186ca9769c960e926870"`);
await queryRunner.query(`DROP TABLE "user_roles"`);
}

}
2 changes: 1 addition & 1 deletion backend/core/src/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ async function seed() {
}
}

admin.isAdmin = true
// admin.roles.isAdmin = true
await userRepo.save(admin)
await userService.confirm({ token: admin.confirmationToken })

Expand Down
16 changes: 14 additions & 2 deletions backend/core/types/src/backend-swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3411,10 +3411,18 @@ export interface LoginResponse {
accessToken: string
}

export interface User {
export interface UserRoles {
/** */
user: User

/** */
roles: UserRole[]
isAdmin: boolean

/** */
isPartner: boolean
}

export interface User {
/** */
language?: Language

Expand Down Expand Up @@ -3447,6 +3455,9 @@ export interface User {

/** */
updatedAt: Date

/** */
roles?: CombinedUserRolesTypes
}

export interface UserCreate {
Expand Down Expand Up @@ -5382,5 +5393,6 @@ export type CombinedApplicationMailingAddressTypes = AddressUpdate
export type CombinedImageTypes = AssetCreate
export type CombinedLeasingAgentAddressTypes = AddressUpdate
export type CombinedResultTypes = AssetCreate
export type CombinedUserRolesTypes = UserRoles
export type CombinedWhatToExpectTypes = WhatToExpect
export type CombinedJurisdictionTypes = Id
8 changes: 4 additions & 4 deletions sites/partners/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
LocalizedLink,
} from "@bloom-housing/ui-components"
import moment from "moment"
import { UserRole, Listing } from "@bloom-housing/backend-core/types"
import { Listing } from "@bloom-housing/backend-core/types"
import { AgGridReact } from "ag-grid-react"
import { GridOptions } from "ag-grid-community"

Expand All @@ -20,7 +20,7 @@ import { MetaTags } from "../src/MetaTags"
export default function ListingsList() {
const { profile } = useContext(AuthContext)
const leasingAgentInListings = profile.leasingAgentInListings?.map((item) => item.id)
const isAdmin = profile.roles.includes(UserRole.admin)
const isAdmin = profile.roles?.isAdmin || false
class formatLinkCell {
link: HTMLAnchorElement

Expand Down Expand Up @@ -136,7 +136,7 @@ export default function ListingsList() {
const { listingDtos, listingsLoading, listingsError } = useListingsData()
// filter listings to show items depends on user role
const filteredListings = useMemo(() => {
if (profile.roles.includes(UserRole.admin)) return listingDtos
if (isAdmin) return listingDtos

return listingDtos?.reduce((acc, curr) => {
if (leasingAgentInListings.includes(curr.id)) {
Expand All @@ -145,7 +145,7 @@ export default function ListingsList() {

return acc
}, []) as Listing[]
}, [leasingAgentInListings, listingDtos, profile.roles])
}, [leasingAgentInListings, listingDtos, isAdmin])

if (listingsLoading) return "Loading..."
if (listingsError) return "An error has occurred."
Expand Down