Skip to content

Commit

Permalink
Multiple Admins and Account Page improvements (#433)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinejaussoin authored Nov 29, 2022
1 parent 2ebe9cf commit 08c73ea
Show file tree
Hide file tree
Showing 32 changed files with 787 additions and 59 deletions.
3 changes: 3 additions & 0 deletions backend/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ export interface FullUser extends User {
subscriptionsId: string | null;
currency: Currency | null;
plan: Plan | null;
planOwner: string | null;
planOwnerEmail: string | null;
planAdmins: string[] | null;
domain: string | null;
ownPlan: Plan | null;
ownSubscriptionsId: string | null;
Expand Down
33 changes: 32 additions & 1 deletion backend/src/db/actions/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SubscriptionRepository, UserRepository } from '../repositories';
import { Plan, Currency } from '../../common';
import { SubscriptionEntity, UserEntity, UserView } from '../entities';
import { transaction } from './transaction';
import { In } from 'typeorm';

export async function activateSubscription(
userId: string,
Expand Down Expand Up @@ -50,7 +51,7 @@ export async function cancelSubscription(
});
}

export async function getActiveSubscription(
export async function getActiveSubscriptionWhereUserIsOwner(
userId: string
): Promise<SubscriptionEntity | null> {
return await transaction(async (manager) => {
Expand All @@ -75,6 +76,36 @@ export async function getActiveSubscription(
});
}

export async function getActiveSubscriptionWhereUserIsAdmin(
userId: string,
email: string | null
): Promise<SubscriptionEntity | null> {
return await transaction(async (manager) => {
const subscriptionRepository = manager.getCustomRepository(
SubscriptionRepository
);

const ids = await subscriptionRepository.query(
`
select s.id from subscriptions s
where s.active = true
and (s.owner_id = $1 or s.admins @> $2)
order by s.updated desc
`,
[userId, `{${email}}`]
);

const subscriptions = await subscriptionRepository.find({
where: { id: In(ids.map((id: { id: string }) => id.id)) },
});

if (subscriptions.length === 0) {
return null;
}
return subscriptions[0];
});
}

export async function saveSubscription(
subscription: SubscriptionEntity
): Promise<void> {
Expand Down
3 changes: 3 additions & 0 deletions backend/src/db/entities/Subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export default class SubscriptionEntity {
public domain: string | null;
@Column('text', { array: true, default: '{}' })
public members: string[];
@Column('text', { array: true, default: '{}' })
public admins: string[];
@CreateDateColumn({ type: 'timestamp with time zone', select: false })
public created: Date | undefined;
@UpdateDateColumn({ type: 'timestamp with time zone', select: false })
Expand All @@ -36,5 +38,6 @@ export default class SubscriptionEntity {
this.plan = plan;
this.domain = null;
this.members = [];
this.admins = [];
}
}
38 changes: 29 additions & 9 deletions backend/src/db/entities/UserView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AccountType, FullUser, Currency, Plan } from '../../common';

@ViewEntity({
expression: `
select
select
u.id,
i.id as identity_id,
u.name,
Expand All @@ -16,18 +16,26 @@ select
u.email,
case when i.account_type = 'anonymous' and i.password is null then false else true end as "can_delete_session",
u.trial,
s.id as "own_subscriptions_id",
s.plan as "own_plan",
coalesce(s.id, s2.id, s3.id) as "subscriptions_id",
coalesce(s.active, s2.active, s3.active, false) as "pro",
coalesce(s.plan, s2.plan, s3.plan) as "plan",
coalesce(s.domain, s2.domain, s3.domain) as "domain"
s1.id as "own_subscriptions_id",
s1.plan as "own_plan",
coalesce(s1.id, s2.id, s3.id) as "subscriptions_id",
coalesce(s1.active, s2.active, s3.active, false) as "pro", /* s4 should not be taken into account for Pro */
coalesce(s1.plan, s2.plan, s3.plan, s4.plan) as "plan",
coalesce(s1.domain, s2.domain, s3.domain, s4.domain) as "domain",
coalesce(o1.name, o2.name, o3.name, o4.name) as "plan_owner",
coalesce(o1.email, o2.email, o3.email, o4.email) as "plan_owner_email",
coalesce(s1.admins, s2.admins, s3.admins, s4.admins) as "plan_admins"
from users_identities i
join users u on u.id = i.user_id
left join subscriptions s on s.owner_id = u.id and s.active is true
left join subscriptions s2 on lower(u.email) = any(lower(s2.members::text)::text[]) and s2.active is true
left join subscriptions s1 on s1.owner_id = u.id and s1.active is true
left join users o1 on o1.id = s1.owner_id
left join subscriptions s2 on s2.members @> ARRAY[u.email::text] and s2.active is true
left join users o2 on o2.id = s2.owner_id
left join subscriptions s3 on s3.domain = split_part(u.email, '@', 2) and s3.active is true
left join users o3 on o3.id = s3.owner_id
left join subscriptions s4 on s4.admins @> ARRAY[u.email::text] and s4.active is true
left join users o4 on o4.id = s4.owner_id
`,
})
export default class UserView {
Expand Down Expand Up @@ -56,6 +64,12 @@ export default class UserView {
@ViewColumn()
public ownPlan: Plan | null;
@ViewColumn()
public planOwner: string | null;
@ViewColumn()
public planOwnerEmail: string | null;
@ViewColumn()
public planAdmins: string[] | null;
@ViewColumn()
public subscriptionsId: string | null;
@ViewColumn()
public plan: Plan | null;
Expand Down Expand Up @@ -83,6 +97,9 @@ export default class UserView {
this.canDeleteSession = false;
this.currency = null;
this.ownPlan = null;
this.planOwner = null;
this.planOwnerEmail = null;
this.planAdmins = null;
this.ownSubscriptionsId = null;
this.plan = null;
this.domain = null;
Expand All @@ -105,6 +122,9 @@ export default class UserView {
stripeId: this.stripeId,
currency: this.currency,
plan: this.plan,
planOwner: this.planOwner,
planOwnerEmail: this.planOwnerEmail,
planAdmins: this.planAdmins,
domain: this.domain,
ownPlan: this.ownPlan,
ownSubscriptionsId: this.ownSubscriptionsId,
Expand Down
75 changes: 75 additions & 0 deletions backend/src/db/migrations/1669661894373-PlanOwners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class PlanOwner1669661894373 implements MigrationInterface {
name = 'planOwner1669661894373'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["VIEW","user_view","public"]);
await queryRunner.query(`DROP VIEW "user_view"`);
await queryRunner.query(`CREATE VIEW "user_view" AS
select
u.id,
i.id as identity_id,
u.name,
i.account_type,
i.username,
u.currency,
u.stripe_id,
i.photo,
u.language,
u.email,
case when i.account_type = 'anonymous' and i.password is null then false else true end as "can_delete_session",
u.trial,
s1.id as "own_subscriptions_id",
s1.plan as "own_plan",
coalesce(s1.id, s2.id, s3.id) as "subscriptions_id",
coalesce(s1.active, s2.active, s3.active, false) as "pro",
coalesce(s1.plan, s2.plan, s3.plan) as "plan",
coalesce(s1.domain, s2.domain, s3.domain) as "domain",
coalesce(o1.name, o2.name, o2.name) as "plan_owner",
coalesce(o1.email, o2.email, o2.email) as "plan_owner_email"
from users_identities i
join users u on u.id = i.user_id
left join subscriptions s1 on s1.owner_id = u.id and s1.active is true
left join users o1 on o1.id = s1.owner_id
left join subscriptions s2 on lower(u.email) = any(lower(s2.members::text)::text[]) and s2.active is true
left join users o2 on o2.id = s2.owner_id
left join subscriptions s3 on s3.domain = split_part(u.email, '@', 2) and s3.active is true
left join users o3 on o3.id = s3.owner_id
`);
await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","VIEW","user_view","select \n u.id,\n i.id as identity_id,\n u.name,\n i.account_type,\n i.username,\n u.currency,\n u.stripe_id,\n i.photo,\n u.language,\n u.email,\n case when i.account_type = 'anonymous' and i.password is null then false else true end as \"can_delete_session\",\n u.trial,\n s1.id as \"own_subscriptions_id\",\n s1.plan as \"own_plan\",\n coalesce(s1.id, s2.id, s3.id) as \"subscriptions_id\",\n coalesce(s1.active, s2.active, s3.active, false) as \"pro\",\n coalesce(s1.plan, s2.plan, s3.plan) as \"plan\",\n coalesce(s1.domain, s2.domain, s3.domain) as \"domain\",\n coalesce(o1.name, o2.name, o2.name) as \"plan_owner\",\n coalesce(o1.email, o2.email, o2.email) as \"plan_owner_email\"\nfrom users_identities i\n\njoin users u on u.id = i.user_id\nleft join subscriptions s1 on s1.owner_id = u.id and s1.active is true\nleft join users o1 on o1.id = s1.owner_id\nleft join subscriptions s2 on lower(u.email) = any(lower(s2.members::text)::text[]) and s2.active is true\nleft join users o2 on o2.id = s2.owner_id\nleft join subscriptions s3 on s3.domain = split_part(u.email, '@', 2) and s3.active is true\nleft join users o3 on o3.id = s3.owner_id"]);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["VIEW","user_view","public"]);
await queryRunner.query(`DROP VIEW "user_view"`);
await queryRunner.query(`CREATE VIEW "user_view" AS select
u.id,
i.id as identity_id,
u.name,
i.account_type,
i.username,
u.currency,
u.stripe_id,
i.photo,
u.language,
u.email,
case when i.account_type = 'anonymous' and i.password is null then false else true end as "can_delete_session",
u.trial,
s.id as "own_subscriptions_id",
s.plan as "own_plan",
coalesce(s.id, s2.id, s3.id) as "subscriptions_id",
coalesce(s.active, s2.active, s3.active, false) as "pro",
coalesce(s.plan, s2.plan, s3.plan) as "plan",
coalesce(s.domain, s2.domain, s3.domain) as "domain"
from users_identities i
join users u on u.id = i.user_id
left join subscriptions s on s.owner_id = u.id and s.active is true
left join subscriptions s2 on lower(u.email) = any(lower(s2.members::text)::text[]) and s2.active is true
left join subscriptions s3 on s3.domain = split_part(u.email, '@', 2) and s3.active is true`);
await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","VIEW","user_view","select \n u.id,\n i.id as identity_id,\n u.name,\n i.account_type,\n i.username,\n u.currency,\n u.stripe_id,\n i.photo,\n u.language,\n u.email,\n case when i.account_type = 'anonymous' and i.password is null then false else true end as \"can_delete_session\",\n u.trial,\n s.id as \"own_subscriptions_id\",\n s.plan as \"own_plan\",\n coalesce(s.id, s2.id, s3.id) as \"subscriptions_id\",\n coalesce(s.active, s2.active, s3.active, false) as \"pro\",\n coalesce(s.plan, s2.plan, s3.plan) as \"plan\",\n coalesce(s.domain, s2.domain, s3.domain) as \"domain\"\nfrom users_identities i\n\njoin users u on u.id = i.user_id\nleft join subscriptions s on s.owner_id = u.id and s.active is true\nleft join subscriptions s2 on lower(u.email) = any(lower(s2.members::text)::text[]) and s2.active is true\nleft join subscriptions s3 on s3.domain = split_part(u.email, '@', 2) and s3.active is true"]);
}

}
15 changes: 15 additions & 0 deletions backend/src/db/migrations/1669662836558-SubscriptionAdmins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class SubscriptionAdmins1669662836558 implements MigrationInterface {
name = 'SubscriptionAdmins1669662836558';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "subscriptions" ADD "admins" text array NOT NULL DEFAULT '{}'`
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "subscriptions" DROP COLUMN "admins"`);
}
}
81 changes: 81 additions & 0 deletions backend/src/db/migrations/1669663848592-AdminsView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class AdminsView1669663848592 implements MigrationInterface {
name = 'AdminsView1669663848592'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["VIEW","user_view","public"]);
await queryRunner.query(`DROP VIEW "user_view"`);
await queryRunner.query(`CREATE VIEW "user_view" AS
select
u.id,
i.id as identity_id,
u.name,
i.account_type,
i.username,
u.currency,
u.stripe_id,
i.photo,
u.language,
u.email,
case when i.account_type = 'anonymous' and i.password is null then false else true end as "can_delete_session",
u.trial,
s1.id as "own_subscriptions_id",
s1.plan as "own_plan",
coalesce(s1.id, s2.id, s3.id) as "subscriptions_id",
coalesce(s1.active, s2.active, s3.active, false) as "pro",
coalesce(s1.plan, s2.plan, s3.plan) as "plan",
coalesce(s1.domain, s2.domain, s3.domain) as "domain",
coalesce(o1.name, o2.name, o3.name) as "plan_owner",
coalesce(o1.email, o2.email, o3.email) as "plan_owner_email",
coalesce(s1.admins, s2.admins, s3.admins) as "plan_admins"
from users_identities i
join users u on u.id = i.user_id
left join subscriptions s1 on s1.owner_id = u.id and s1.active is true
left join users o1 on o1.id = s1.owner_id
left join subscriptions s2 on lower(u.email) = any(lower(s2.members::text)::text[]) and s2.active is true
left join users o2 on o2.id = s2.owner_id
left join subscriptions s3 on s3.domain = split_part(u.email, '@', 2) and s3.active is true
left join users o3 on o3.id = s3.owner_id
`);
await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","VIEW","user_view","select \n u.id,\n i.id as identity_id,\n u.name,\n i.account_type,\n i.username,\n u.currency,\n u.stripe_id,\n i.photo,\n u.language,\n u.email,\n case when i.account_type = 'anonymous' and i.password is null then false else true end as \"can_delete_session\",\n u.trial,\n s1.id as \"own_subscriptions_id\",\n s1.plan as \"own_plan\",\n coalesce(s1.id, s2.id, s3.id) as \"subscriptions_id\",\n coalesce(s1.active, s2.active, s3.active, false) as \"pro\",\n coalesce(s1.plan, s2.plan, s3.plan) as \"plan\",\n coalesce(s1.domain, s2.domain, s3.domain) as \"domain\",\n coalesce(o1.name, o2.name, o3.name) as \"plan_owner\",\n coalesce(o1.email, o2.email, o3.email) as \"plan_owner_email\",\n coalesce(s1.admins, s2.admins, s3.admins) as \"plan_admins\"\nfrom users_identities i\n\njoin users u on u.id = i.user_id\nleft join subscriptions s1 on s1.owner_id = u.id and s1.active is true\nleft join users o1 on o1.id = s1.owner_id\nleft join subscriptions s2 on lower(u.email) = any(lower(s2.members::text)::text[]) and s2.active is true\nleft join users o2 on o2.id = s2.owner_id\nleft join subscriptions s3 on s3.domain = split_part(u.email, '@', 2) and s3.active is true\nleft join users o3 on o3.id = s3.owner_id"]);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["VIEW","user_view","public"]);
await queryRunner.query(`DROP VIEW "user_view"`);
await queryRunner.query(`CREATE VIEW "user_view" AS select
u.id,
i.id as identity_id,
u.name,
i.account_type,
i.username,
u.currency,
u.stripe_id,
i.photo,
u.language,
u.email,
case when i.account_type = 'anonymous' and i.password is null then false else true end as "can_delete_session",
u.trial,
s1.id as "own_subscriptions_id",
s1.plan as "own_plan",
coalesce(s1.id, s2.id, s3.id) as "subscriptions_id",
coalesce(s1.active, s2.active, s3.active, false) as "pro",
coalesce(s1.plan, s2.plan, s3.plan) as "plan",
coalesce(s1.domain, s2.domain, s3.domain) as "domain",
coalesce(o1.name, o2.name, o2.name) as "plan_owner",
coalesce(o1.email, o2.email, o2.email) as "plan_owner_email"
from users_identities i
join users u on u.id = i.user_id
left join subscriptions s1 on s1.owner_id = u.id and s1.active is true
left join users o1 on o1.id = s1.owner_id
left join subscriptions s2 on lower(u.email) = any(lower(s2.members::text)::text[]) and s2.active is true
left join users o2 on o2.id = s2.owner_id
left join subscriptions s3 on s3.domain = split_part(u.email, '@', 2) and s3.active is true
left join users o3 on o3.id = s3.owner_id`);
await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","VIEW","user_view","select \n u.id,\n i.id as identity_id,\n u.name,\n i.account_type,\n i.username,\n u.currency,\n u.stripe_id,\n i.photo,\n u.language,\n u.email,\n case when i.account_type = 'anonymous' and i.password is null then false else true end as \"can_delete_session\",\n u.trial,\n s1.id as \"own_subscriptions_id\",\n s1.plan as \"own_plan\",\n coalesce(s1.id, s2.id, s3.id) as \"subscriptions_id\",\n coalesce(s1.active, s2.active, s3.active, false) as \"pro\",\n coalesce(s1.plan, s2.plan, s3.plan) as \"plan\",\n coalesce(s1.domain, s2.domain, s3.domain) as \"domain\",\n coalesce(o1.name, o2.name, o2.name) as \"plan_owner\",\n coalesce(o1.email, o2.email, o2.email) as \"plan_owner_email\"\nfrom users_identities i\n\njoin users u on u.id = i.user_id\nleft join subscriptions s1 on s1.owner_id = u.id and s1.active is true\nleft join users o1 on o1.id = s1.owner_id\nleft join subscriptions s2 on lower(u.email) = any(lower(s2.members::text)::text[]) and s2.active is true\nleft join users o2 on o2.id = s2.owner_id\nleft join subscriptions s3 on s3.domain = split_part(u.email, '@', 2) and s3.active is true\nleft join users o3 on o3.id = s3.owner_id"]);
}

}
Loading

0 comments on commit 08c73ea

Please sign in to comment.