Browse Source

Merge pull request #8027 from nocodb/nc-org-migrations

Support multiple refresh token
pull/8040/head
Pranav C 9 months ago committed by GitHub
parent
commit
7ff03a510a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      packages/nocodb/src/helpers/initAdminFromEnv.ts
  2. 2
      packages/nocodb/src/meta/meta.service.ts
  3. 4
      packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts
  4. 6
      packages/nocodb/src/meta/migrations/v2/nc_011.ts
  5. 8
      packages/nocodb/src/meta/migrations/v2/nc_032_cleanup.ts
  6. 27
      packages/nocodb/src/meta/migrations/v2/nc_043_user_refresh_token.ts
  7. 22
      packages/nocodb/src/models/User.ts
  8. 111
      packages/nocodb/src/models/UserRefreshToken.ts
  9. 1
      packages/nocodb/src/models/index.ts
  10. 26
      packages/nocodb/src/services/users/users.service.ts
  11. 7
      packages/nocodb/src/utils/globals.ts
  12. 1
      packages/nocodb/src/utils/sanitiseUserObj.ts

4
packages/nocodb/src/helpers/initAdminFromEnv.ts

@ -185,7 +185,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
password, password,
email_verification_token, email_verification_token,
token_version: randomTokenString(), token_version: randomTokenString(),
refresh_token: null,
}, },
ncMeta, ncMeta,
); );
@ -199,7 +198,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
password, password,
email_verification_token, email_verification_token,
token_version: randomTokenString(), token_version: randomTokenString(),
refresh_token: null,
}, },
ncMeta, ncMeta,
); );
@ -220,7 +218,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
password, password,
email_verification_token, email_verification_token,
token_version: randomTokenString(), token_version: randomTokenString(),
refresh_token: null,
}, },
ncMeta, ncMeta,
); );
@ -248,7 +245,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
password, password,
email_verification_token, email_verification_token,
token_version: randomTokenString(), token_version: randomTokenString(),
refresh_token: null,
roles, roles,
}, },
ncMeta, ncMeta,

2
packages/nocodb/src/meta/meta.service.ts

@ -228,7 +228,7 @@ export class MetaService {
case MetaTable.USERS: case MetaTable.USERS:
prefix = 'us'; prefix = 'us';
break; break;
case MetaTable.ORGS: case MetaTable.ORGS_OLD:
prefix = 'org'; prefix = 'org';
break; break;
case MetaTable.TEAMS: case MetaTable.TEAMS:

4
packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts

@ -29,6 +29,7 @@ import * as nc_039_sqlite_alter_column_types from '~/meta/migrations/v2/nc_039_s
import * as nc_040_form_view_alter_column_types from '~/meta/migrations/v2/nc_040_form_view_alter_column_types'; import * as nc_040_form_view_alter_column_types from '~/meta/migrations/v2/nc_040_form_view_alter_column_types';
import * as nc_041_calendar_view from '~/meta/migrations/v2/nc_041_calendar_view'; import * as nc_041_calendar_view from '~/meta/migrations/v2/nc_041_calendar_view';
import * as nc_042_user_block from '~/meta/migrations/v2/nc_042_user_block'; import * as nc_042_user_block from '~/meta/migrations/v2/nc_042_user_block';
import * as nc_043_user_refresh_token from '~/meta/migrations/v2/nc_043_user_refresh_token';
// Create a custom migration source class // Create a custom migration source class
export default class XcMigrationSourcev2 { export default class XcMigrationSourcev2 {
@ -69,6 +70,7 @@ export default class XcMigrationSourcev2 {
'nc_040_form_view_alter_column_types', 'nc_040_form_view_alter_column_types',
'nc_041_calendar_view', 'nc_041_calendar_view',
'nc_042_user_block', 'nc_042_user_block',
'nc_043_user_refresh_token',
]); ]);
} }
@ -140,6 +142,8 @@ export default class XcMigrationSourcev2 {
return nc_041_calendar_view; return nc_041_calendar_view;
case 'nc_042_user_block': case 'nc_042_user_block':
return nc_042_user_block; return nc_042_user_block;
case 'nc_043_user_refresh_token':
return nc_043_user_refresh_token;
} }
} }
} }

6
packages/nocodb/src/meta/migrations/v2/nc_011.ts

@ -700,7 +700,7 @@ const up = async (knex) => {
table.timestamps(true, true); table.timestamps(true, true);
}); });
await knex.schema.createTable(MetaTable.ORGS, (table) => { await knex.schema.createTable(MetaTable.ORGS_OLD, (table) => {
table.string('id', 20).primary().notNullable(); table.string('id', 20).primary().notNullable();
table.string('title'); table.string('title');
@ -712,13 +712,13 @@ const up = async (knex) => {
table.string('title'); table.string('title');
table.string('org_id', 20); table.string('org_id', 20);
table.foreign('org_id').references(`${MetaTable.ORGS}.id`); table.foreign('org_id').references(`${MetaTable.ORGS_OLD}.id`);
table.timestamps(true, true); table.timestamps(true, true);
}); });
await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => { await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => {
table.string('org_id', 20); table.string('org_id', 20);
table.foreign('org_id').references(`${MetaTable.ORGS}.id`); table.foreign('org_id').references(`${MetaTable.ORGS_OLD}.id`);
table.string('user_id', 20); table.string('user_id', 20);
table.foreign('user_id').references(`${MetaTable.USERS}.id`); table.foreign('user_id').references(`${MetaTable.USERS}.id`);
table.timestamps(true, true); table.timestamps(true, true);

8
packages/nocodb/src/meta/migrations/v2/nc_032_cleanup.ts

@ -7,11 +7,11 @@ const up = async (knex: Knex) => {
await knex.schema.dropTable(MetaTable.TEAMS); await knex.schema.dropTable(MetaTable.TEAMS);
await knex.schema.dropTable(MetaTable.ORGS); await knex.schema.dropTable(MetaTable.ORGS_OLD);
}; };
const down = async (knex: Knex) => { const down = async (knex: Knex) => {
await knex.schema.createTable(MetaTable.ORGS, (table) => { await knex.schema.createTable(MetaTable.ORGS_OLD, (table) => {
table.string('id', 20).primary().notNullable(); table.string('id', 20).primary().notNullable();
table.string('title'); table.string('title');
@ -23,13 +23,13 @@ const down = async (knex: Knex) => {
table.string('title'); table.string('title');
table.string('org_id', 20); table.string('org_id', 20);
table.foreign('org_id').references(`${MetaTable.ORGS}.id`); table.foreign('org_id').references(`${MetaTable.ORGS_OLD}.id`);
table.timestamps(true, true); table.timestamps(true, true);
}); });
await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => { await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => {
table.string('org_id', 20); table.string('org_id', 20);
table.foreign('org_id').references(`${MetaTable.ORGS}.id`); table.foreign('org_id').references(`${MetaTable.ORGS_OLD}.id`);
table.string('user_id', 20); table.string('user_id', 20);
table.foreign('user_id').references(`${MetaTable.USERS}.id`); table.foreign('user_id').references(`${MetaTable.USERS}.id`);
table.timestamps(true, true); table.timestamps(true, true);

27
packages/nocodb/src/meta/migrations/v2/nc_043_user_refresh_token.ts

@ -0,0 +1,27 @@
import type { Knex } from 'knex';
import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.USERS, (table) => {
table.dropColumn('refresh_token');
});
await knex.schema.createTable(MetaTable.USER_REFRESH_TOKENS, (table) => {
table.string('fk_user_id', 20).index();
table.string('token', 255).index();
table.text('meta');
table.timestamp('expires_at').index();
table.timestamps(true, true);
});
};
const down = async (knex: Knex) => {
await knex.schema.dropTable(MetaTable.USER_REFRESH_TOKENS);
await knex.schema.alterTable(MetaTable.USERS, (table) => {
table.string('refresh_token', 255);
});
};
export { up, down };

22
packages/nocodb/src/models/User.ts

@ -9,7 +9,7 @@ import {
CacheScope, CacheScope,
MetaTable, MetaTable,
} from '~/utils/globals'; } from '~/utils/globals';
import { Base, BaseUser } from '~/models'; import { Base, BaseUser, UserRefreshToken } from '~/models';
import { sanitiseUserObj } from '~/utils'; import { sanitiseUserObj } from '~/utils';
export default class User implements UserType { export default class User implements UserType {
@ -20,7 +20,6 @@ export default class User implements UserType {
password?: string; password?: string;
salt?: string; salt?: string;
refresh_token?: string;
invite_token?: string; invite_token?: string;
invite_token_expires?: number | Date; invite_token_expires?: number | Date;
reset_password_expires?: number | Date; reset_password_expires?: number | Date;
@ -50,7 +49,6 @@ export default class User implements UserType {
'email', 'email',
'password', 'password',
'salt', 'salt',
'refresh_token',
'invite_token', 'invite_token',
'invite_token_expires', 'invite_token_expires',
'reset_password_expires', 'reset_password_expires',
@ -91,7 +89,6 @@ export default class User implements UserType {
'email', 'email',
'password', 'password',
'salt', 'salt',
'refresh_token',
'invite_token', 'invite_token',
'invite_token_expires', 'invite_token_expires',
'reset_password_expires', 'reset_password_expires',
@ -184,9 +181,21 @@ export default class User implements UserType {
} }
static async getByRefreshToken(refresh_token, ncMeta = Noco.ncMeta) { static async getByRefreshToken(refresh_token, ncMeta = Noco.ncMeta) {
return await ncMeta.metaGet2(null, null, MetaTable.USERS, { const userRefreshToken = await UserRefreshToken.getByToken(
refresh_token, refresh_token,
}); ncMeta,
);
if (!userRefreshToken) {
return null;
}
return await ncMeta.metaGet2(
null,
null,
MetaTable.USERS,
userRefreshToken.fk_user_id,
);
} }
public static async list( public static async list(
@ -262,6 +271,7 @@ export default class User implements UserType {
args: { args: {
user?: User; user?: User;
baseId?: string; baseId?: string;
orgId?: string;
}, },
ncMeta = Noco.ncMeta, ncMeta = Noco.ncMeta,
) { ) {

111
packages/nocodb/src/models/UserRefreshToken.ts

@ -0,0 +1,111 @@
import dayjs from 'dayjs';
import Noco from '~/Noco';
import { extractProps } from '~/helpers/extractProps';
import { MetaTable } from '~/utils/globals';
import { parseMetaProp, stringifyMetaProp } from '~/utils/modelUtils';
export default class UserRefreshToken {
fk_user_id: string;
token: string;
expires_at: any;
meta?: any;
created_at?: any;
updated_at?: any;
public static async insert(
userRefreshToken: Partial<UserRefreshToken>,
ncMeta = Noco.ncMeta,
) {
// clear old invalid tokens before inserting new one
// todo: verify the populated sql query
await ncMeta.metaDelete(
null,
null,
MetaTable.USER_REFRESH_TOKENS,
{
fk_user_id: userRefreshToken.fk_user_id,
},
{
expires_at: {
lt: dayjs().toDate(),
},
},
);
const insertObj = extractProps(userRefreshToken, [
'fk_user_id',
'token',
'expires_at',
'meta',
]);
// set default expiry as 90 days if missing
if (!('expires_at' in insertObj)) {
insertObj.expires_at = dayjs().add(90, 'day').toDate();
}
if ('meta' in insertObj) {
insertObj.meta = stringifyMetaProp(insertObj);
}
await ncMeta.metaInsert2(
null,
null,
MetaTable.USER_REFRESH_TOKENS,
insertObj,
true,
);
return insertObj;
}
static async updateOldToken(
oldToken: string,
newToken: string,
ncMeta = Noco.ncMeta,
) {
return await ncMeta.metaUpdate(
null,
null,
MetaTable.USER_REFRESH_TOKENS,
{
token: oldToken,
expires_at: dayjs().add(90, 'day').toDate(),
},
{
token: newToken,
},
);
}
static async deleteToken(token: string, ncMeta = Noco.ncMeta) {
return await ncMeta.metaDelete(null, null, MetaTable.USER_REFRESH_TOKENS, {
token,
});
}
static async deleteAllUserToken(userId: string, ncMeta = Noco.ncMeta) {
return await ncMeta.metaDelete(null, null, MetaTable.USER_REFRESH_TOKENS, {
fk_user_id: userId,
});
}
static async getByToken(
token: string,
ncMeta = Noco.ncMeta,
): Promise<UserRefreshToken> {
const userToken = await ncMeta.metaGet2(
null,
null,
MetaTable.USER_REFRESH_TOKENS,
{
token,
},
);
if (!userToken) return null;
userToken.meta = parseMetaProp(userToken);
return userToken;
}
}

1
packages/nocodb/src/models/index.ts

@ -40,3 +40,4 @@ export { default as View } from './View';
export { default as LinksColumn } from './LinksColumn'; export { default as LinksColumn } from './LinksColumn';
export { default as Notification } from './Notification'; export { default as Notification } from './Notification';
export { default as PresignedUrl } from './PresignedUrl'; export { default as PresignedUrl } from './PresignedUrl';
export { default as UserRefreshToken } from './UserRefreshToken';

26
packages/nocodb/src/services/users/users.service.ts

@ -21,7 +21,7 @@ import { validatePayload } from '~/helpers';
import { MetaService } from '~/meta/meta.service'; import { MetaService } from '~/meta/meta.service';
import { MetaTable } from '~/utils/globals'; import { MetaTable } from '~/utils/globals';
import Noco from '~/Noco'; import Noco from '~/Noco';
import { Store, User } from '~/models'; import { Store, User, UserRefreshToken } from '~/models';
import { randomTokenString } from '~/helpers/stringHelpers'; import { randomTokenString } from '~/helpers/stringHelpers';
import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2'; import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
@ -380,9 +380,9 @@ export class UsersService {
const refreshToken = randomTokenString(); const refreshToken = randomTokenString();
await User.update(user.id, { await UserRefreshToken.insert({
email: user.email, token: refreshToken,
refresh_token: refreshToken, fk_user_id: user.id,
}); });
setTokenCookie(param.res, refreshToken); setTokenCookie(param.res, refreshToken);
@ -495,9 +495,9 @@ export class UsersService {
const refreshToken = randomTokenString(); const refreshToken = randomTokenString();
await User.update(user.id, { await UserRefreshToken.insert({
refresh_token: refreshToken, token: refreshToken,
email: user.email, fk_user_id: user.id,
}); });
setTokenCookie(param.res, refreshToken); setTokenCookie(param.res, refreshToken);
@ -532,9 +532,10 @@ export class UsersService {
const user = (param.req as any).user; const user = (param.req as any).user;
if (user?.id) { if (user?.id) {
await User.update(user.id, { await User.update(user.id, {
refresh_token: null,
token_version: randomTokenString(), token_version: randomTokenString(),
}); });
// todo: clear only token present in cookie to avoid invalidating all refresh token
await UserRefreshToken.deleteAllUserToken(user.id);
} }
return { msg: 'Signed out successfully' }; return { msg: 'Signed out successfully' };
} catch (e) { } catch (e) {
@ -572,10 +573,15 @@ export class UsersService {
} }
await User.update(user.id, { await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user['token_version'], token_version: user['token_version'],
}); });
await UserRefreshToken.insert({
token: refreshToken,
fk_user_id: user.id,
meta: req.user?.extra,
});
setTokenCookie(res, refreshToken); setTokenCookie(res, refreshToken);
} }
} }

7
packages/nocodb/src/utils/globals.ts

@ -29,7 +29,7 @@ export enum MetaTable {
KANBAN_VIEW = 'nc_kanban_view_v2', KANBAN_VIEW = 'nc_kanban_view_v2',
KANBAN_VIEW_COLUMNS = 'nc_kanban_view_columns_v2', KANBAN_VIEW_COLUMNS = 'nc_kanban_view_columns_v2',
USERS = 'nc_users_v2', USERS = 'nc_users_v2',
ORGS = 'nc_orgs_v2', ORGS_OLD = 'nc_orgs_v2',
TEAMS = 'nc_teams_v2', TEAMS = 'nc_teams_v2',
TEAM_USERS = 'nc_team_users_v2', TEAM_USERS = 'nc_team_users_v2',
VIEWS = 'nc_views_v2', VIEWS = 'nc_views_v2',
@ -46,6 +46,7 @@ export enum MetaTable {
MAP_VIEW_COLUMNS = 'nc_map_view_columns_v2', MAP_VIEW_COLUMNS = 'nc_map_view_columns_v2',
STORE = 'nc_store', STORE = 'nc_store',
NOTIFICATION = 'notification', NOTIFICATION = 'notification',
USER_REFRESH_TOKENS = 'nc_user_refresh_tokens',
} }
export enum MetaTableOldV2 { export enum MetaTableOldV2 {
@ -60,7 +61,7 @@ export const orderedMetaTables = [
MetaTable.AUDIT, MetaTable.AUDIT,
MetaTable.TEAM_USERS, MetaTable.TEAM_USERS,
MetaTable.TEAMS, MetaTable.TEAMS,
MetaTable.ORGS, MetaTable.ORGS_OLD,
MetaTable.PROJECT_USERS, MetaTable.PROJECT_USERS,
MetaTable.USERS, MetaTable.USERS,
MetaTable.MAP_VIEW, MetaTable.MAP_VIEW,
@ -151,7 +152,7 @@ export enum CacheScope {
MAP_VIEW_COLUMN = 'mapViewColumn', MAP_VIEW_COLUMN = 'mapViewColumn',
KANBAN_VIEW_COLUMN = 'kanbanViewColumn', KANBAN_VIEW_COLUMN = 'kanbanViewColumn',
USER = 'user', USER = 'user',
ORGS = 'orgs', ORGS_OLD = 'orgs',
TEAM = 'team', TEAM = 'team',
TEAM_USER = 'teamUser', TEAM_USER = 'teamUser',
VIEW = 'view', VIEW = 'view',

1
packages/nocodb/src/utils/sanitiseUserObj.ts

@ -1,7 +1,6 @@
const ignoreKeys = new Set([ const ignoreKeys = new Set([
'password', 'password',
'salt', 'salt',
'refresh_token',
'invite_token', 'invite_token',
'invite_token_expires', 'invite_token_expires',
'reset_password_expires', 'reset_password_expires',

Loading…
Cancel
Save