Skip to content

Commit 98b4fc0

Browse files
feat: treat orgs user_type
1 parent cf2c50a commit 98b4fc0

5 files changed

Lines changed: 35 additions & 18 deletions

File tree

‎migrations/dashboard/create-tables.js.sql‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ CREATE TABLE IF NOT EXISTS gp_orgs (
102102
github_id VARCHAR(255) NOT NULL,
103103
adoption_token VARCHAR(255) NOT NULL,
104104
extra_adoption_tokens LONGTEXT COLLATE utf8mb4_bin NOT NULL DEFAULT '[]' CHECK (json_valid(`extra_adoption_tokens`)),
105-
public_probes BOOLEAN NOT NULL DEFAULT 0
105+
public_probes BOOLEAN NOT NULL DEFAULT 0,
106+
user_type VARCHAR(255) NOT NULL DEFAULT 'member'
106107
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
107108

108109
CREATE TABLE IF NOT EXISTS gp_org_members (

‎src/lib/accounts.ts‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { dashboardClient } from './sql/client.js';
33

44
export const ACCOUNTS_TABLE = 'gp_accounts';
55
export const MEMBERS_TABLE = 'gp_org_members';
6+
const ORGS_TABLE = 'gp_orgs';
67
const ACCOUNT_TTL = 60 * 60 * 1000;
78
const ROLE_TTL = 60 * 1000;
89

910
export type AccountRole = 'owner' | 'admin' | 'member' | 'viewer';
10-
export type ResolvedAccount = { id: string; role: AccountRole };
11+
export type ResolvedAccount = { id: string; role: AccountRole; userType: 'member' | 'sponsor' | 'special' };
1112

1213
const accountIdsByUserId = new TTLCache<string, string>({ ttl: ACCOUNT_TTL });
1314
const rolesByUserAndAccount = new TTLCache<string, ResolvedAccount | null>({ ttl: ROLE_TTL });
@@ -29,7 +30,7 @@ export const getUserAccountId = async (userId: string): Promise<string | null> =
2930
};
3031

3132
// The role of the user in the account: 'owner' for their own, their membership role for an org, null if they are not in it.
32-
export const getAccountRole = async (accountId: string, userId: string): Promise<ResolvedAccount | null> => {
33+
export const getOrgRole = async (accountId: string, userId: string): Promise<ResolvedAccount | null> => {
3334
const key = `${userId}:${accountId}`;
3435
const cached = rolesByUserAndAccount.get(key);
3536

@@ -38,20 +39,15 @@ export const getAccountRole = async (accountId: string, userId: string): Promise
3839
}
3940

4041
const account = await dashboardClient(ACCOUNTS_TABLE)
41-
.leftJoin(MEMBERS_TABLE, function () {
42+
.join(MEMBERS_TABLE, function () {
4243
this.on(`${MEMBERS_TABLE}.org`, `${ACCOUNTS_TABLE}.org`)
4344
.andOnVal(`${MEMBERS_TABLE}.user`, '=', userId);
4445
})
46+
.join(ORGS_TABLE, `${ORGS_TABLE}.id`, `${ACCOUNTS_TABLE}.org`)
4547
.where(`${ACCOUNTS_TABLE}.id`, accountId)
46-
.where(function () {
47-
this.where(`${ACCOUNTS_TABLE}.user`, userId).orWhereNotNull(`${MEMBERS_TABLE}.id`);
48-
})
49-
.first<{ id: string; role: AccountRole } | undefined>(
50-
`${ACCOUNTS_TABLE}.id`,
51-
dashboardClient.raw(`IF(?? = ?, 'owner', ??) AS role`, [ `${ACCOUNTS_TABLE}.user`, userId, `${MEMBERS_TABLE}.role` ]),
52-
);
48+
.first<ResolvedAccount | undefined>(`${ACCOUNTS_TABLE}.id`, `${MEMBERS_TABLE}.role`, `${ORGS_TABLE}.user_type AS userType`);
5349

54-
const role = account ? { id: account.id, role: account.role } : null;
50+
const role = account ? { id: account.id, role: account.role, userType: account.userType } : null;
5551
rolesByUserAndAccount.set(key, role);
5652

5753
return role;

‎src/lib/http/auth.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ACCOUNTS_TABLE, MEMBERS_TABLE, type AccountRole } from '../accounts.js'
88

99
export const GP_TOKENS_TABLE = 'gp_tokens';
1010
export const USERS_TABLE = 'directus_users';
11+
const ORGS_TABLE = 'gp_orgs';
1112

1213
const logger = scopedLogger('auth');
1314

@@ -97,6 +98,7 @@ export class Auth {
9798
this.on(`${MEMBERS_TABLE}.org`, `${ACCOUNTS_TABLE}.org`)
9899
.andOn(`${MEMBERS_TABLE}.user`, `${GP_TOKENS_TABLE}.user_created`);
99100
})
101+
.leftJoin(ORGS_TABLE, `${ORGS_TABLE}.id`, `${ACCOUNTS_TABLE}.org`)
100102
.where(filter)
101103
.select<Row[]>([
102104
'user_created',
@@ -107,7 +109,7 @@ export class Auth {
107109
'date_last_used',
108110
'scopes',
109111
'github_username as user_github_username',
110-
'user_type as user_user_type',
112+
this.sql.raw(`COALESCE(??, ??) AS user_user_type`, [ `${ORGS_TABLE}.user_type`, `${USERS_TABLE}.user_type` ]),
111113
this.sql.raw(`IF(?? = ??, 'owner', ??) AS account_role`, [ `${ACCOUNTS_TABLE}.user`, `${GP_TOKENS_TABLE}.user_created`, `${MEMBERS_TABLE}.role` ]),
112114
]);
113115

‎src/lib/http/middleware/authenticate.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { jwtVerify } from 'jose';
33
import createHttpError from 'http-errors';
44
import apmAgent from 'elastic-apm-node';
55

6-
import { type AccountRole, getAccountRole, getUserAccountId } from '../../accounts.js';
6+
import { type AccountRole, getOrgRole, getUserAccountId } from '../../accounts.js';
77
import { auth } from '../auth.js';
88
import type { ExtendedMiddleware } from '../../../types.js';
99

@@ -54,17 +54,17 @@ const resolveAccount = async (ctx: Parameters<ExtendedMiddleware>[0], payload: S
5454
cookieUserId !== payload.id
5555
|| !activeAccountId
5656
|| activeAccountId === userAccountId) {
57-
return { accountId: userAccountId, accountRole: 'owner' as AccountRole };
57+
return { accountId: userAccountId, accountRole: 'owner' as const, userType: payload.user_type ?? 'member' };
5858
}
5959

6060
// activeAccountId is a cookie set by dashboard FE so it is untrusted, unlike userAccountId which is signed by the dashboard.
61-
const resolved = await getAccountRole(activeAccountId, payload.id!);
61+
const resolved = await getOrgRole(activeAccountId, payload.id!);
6262

6363
if (!resolved) {
6464
throw createHttpError(403, 'The selected account is not available.', { type: 'access_forbidden' });
6565
}
6666

67-
return { accountId: resolved.id, accountRole: resolved.role };
67+
return { accountId: resolved.id, accountRole: resolved.role, userType: resolved.userType };
6868
};
6969

7070
export const verifySessionPayload = async (cookie: string, key: Uint8Array): Promise<SessionCookiePayload | undefined> => {
@@ -106,7 +106,7 @@ export const authenticate = (): ExtendedMiddleware => {
106106

107107
if (payload?.id && payload.app_access === true) {
108108
const account = await resolveAccount(ctx, payload);
109-
ctx.state.user = { id: payload.id, ...account, username: payload.github_username || null, userType: payload.user_type || 'member', authMode: 'cookie', adminAccess: payload.admin_access === true };
109+
ctx.state.user = { id: payload.id, ...account, username: payload.github_username || null, authMode: 'cookie', adminAccess: payload.admin_access === true };
110110
apmAgent.setUserContext({ id: payload.id, username: payload.github_username || `ID(${payload.id})` });
111111
}
112112
}

‎test/tests/integration/middleware/authenticate.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ describe('authenticate', () => {
4242
await deleteFakeProbes();
4343
});
4444

45+
describe('tier', () => {
46+
it('should take the tier from the org when the token bills an org, and from the user otherwise', async () => {
47+
const sponsor = await createUser(dashboardClient, { user_type: 'sponsor' });
48+
const org = await createOrg(dashboardClient, { user_type: 'member', members: [{ userId: sponsor.id, role: 'admin' }] });
49+
50+
await dashboardClient(GP_TOKENS_TABLE).insert([
51+
{ name: 'personal', user_created: sponsor.id, account_id: sponsor.accountId, value: 'kOSvlLIT0R9OjLXOaEqfgMIoZP8TTFFDnHXNkPJk5s0=' },
52+
{ name: 'org', user_created: sponsor.id, account_id: org.accountId, value: 'RQ0R8MTk4VLUGAxaMGrUvFjNJj39v1MvNwXt0CvTEPY=' },
53+
]);
54+
55+
const [ personalToken ] = await auth.fetchTokens({ account_id: sponsor.accountId });
56+
const [ orgToken ] = await auth.fetchTokens({ account_id: org.accountId });
57+
58+
expect(personalToken!.user_user_type).to.equal('sponsor');
59+
expect(orgToken!.user_user_type).to.equal('member');
60+
});
61+
});
62+
4563
describe('token', () => {
4664
it('should accept if no "Authorization" header was passed', async () => {
4765
await requestAgent.post('/v1/measurements')

0 commit comments

Comments
 (0)