diff --git a/packages/nocodb/src/helpers/initAdminFromEnv.ts b/packages/nocodb/src/helpers/initAdminFromEnv.ts index 4f68d8db28..d627a99bcb 100644 --- a/packages/nocodb/src/helpers/initAdminFromEnv.ts +++ b/packages/nocodb/src/helpers/initAdminFromEnv.ts @@ -185,7 +185,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) { password, email_verification_token, token_version: randomTokenString(), - refresh_token: null, }, ncMeta, ); @@ -199,7 +198,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) { password, email_verification_token, token_version: randomTokenString(), - refresh_token: null, }, ncMeta, ); @@ -220,7 +218,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) { password, email_verification_token, token_version: randomTokenString(), - refresh_token: null, }, ncMeta, ); @@ -248,7 +245,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) { password, email_verification_token, token_version: randomTokenString(), - refresh_token: null, roles, }, ncMeta, diff --git a/packages/nocodb/src/meta/meta.service.ts b/packages/nocodb/src/meta/meta.service.ts index 608e9eb666..ee564124c2 100644 --- a/packages/nocodb/src/meta/meta.service.ts +++ b/packages/nocodb/src/meta/meta.service.ts @@ -228,7 +228,7 @@ export class MetaService { case MetaTable.USERS: prefix = 'us'; break; - case MetaTable.ORGS: + case MetaTable.ORGS_OLD: prefix = 'org'; break; case MetaTable.TEAMS: diff --git a/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts b/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts index 384429d5f5..8702674989 100644 --- a/packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts +++ b/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_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_043_user_refresh_token from '~/meta/migrations/v2/nc_043_user_refresh_token'; // Create a custom migration source class export default class XcMigrationSourcev2 { @@ -69,6 +70,7 @@ export default class XcMigrationSourcev2 { 'nc_040_form_view_alter_column_types', 'nc_041_calendar_view', 'nc_042_user_block', + 'nc_043_user_refresh_token', ]); } @@ -140,6 +142,8 @@ export default class XcMigrationSourcev2 { return nc_041_calendar_view; case 'nc_042_user_block': return nc_042_user_block; + case 'nc_043_user_refresh_token': + return nc_043_user_refresh_token; } } } diff --git a/packages/nocodb/src/meta/migrations/v2/nc_011.ts b/packages/nocodb/src/meta/migrations/v2/nc_011.ts index 7aca8fb30e..126b69318b 100644 --- a/packages/nocodb/src/meta/migrations/v2/nc_011.ts +++ b/packages/nocodb/src/meta/migrations/v2/nc_011.ts @@ -700,7 +700,7 @@ const up = async (knex) => { 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('title'); @@ -712,13 +712,13 @@ const up = async (knex) => { table.string('title'); 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); }); await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => { 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.foreign('user_id').references(`${MetaTable.USERS}.id`); table.timestamps(true, true); diff --git a/packages/nocodb/src/meta/migrations/v2/nc_032_cleanup.ts b/packages/nocodb/src/meta/migrations/v2/nc_032_cleanup.ts index d9b8b72486..dde8c9c645 100644 --- a/packages/nocodb/src/meta/migrations/v2/nc_032_cleanup.ts +++ b/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.ORGS); + await knex.schema.dropTable(MetaTable.ORGS_OLD); }; 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('title'); @@ -23,13 +23,13 @@ const down = async (knex: Knex) => { table.string('title'); 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); }); await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => { 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.foreign('user_id').references(`${MetaTable.USERS}.id`); table.timestamps(true, true); diff --git a/packages/nocodb/src/meta/migrations/v2/nc_043_user_refresh_token.ts b/packages/nocodb/src/meta/migrations/v2/nc_043_user_refresh_token.ts new file mode 100644 index 0000000000..98e9a6b8c3 --- /dev/null +++ b/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 }; diff --git a/packages/nocodb/src/models/User.ts b/packages/nocodb/src/models/User.ts index 6eba62d5eb..447d7fed11 100644 --- a/packages/nocodb/src/models/User.ts +++ b/packages/nocodb/src/models/User.ts @@ -9,7 +9,7 @@ import { CacheScope, MetaTable, } from '~/utils/globals'; -import { Base, BaseUser } from '~/models'; +import { Base, BaseUser, UserRefreshToken } from '~/models'; import { sanitiseUserObj } from '~/utils'; export default class User implements UserType { @@ -20,7 +20,6 @@ export default class User implements UserType { password?: string; salt?: string; - refresh_token?: string; invite_token?: string; invite_token_expires?: number | Date; reset_password_expires?: number | Date; @@ -50,7 +49,6 @@ export default class User implements UserType { 'email', 'password', 'salt', - 'refresh_token', 'invite_token', 'invite_token_expires', 'reset_password_expires', @@ -91,7 +89,6 @@ export default class User implements UserType { 'email', 'password', 'salt', - 'refresh_token', 'invite_token', 'invite_token_expires', 'reset_password_expires', @@ -184,9 +181,21 @@ export default class User implements UserType { } static async getByRefreshToken(refresh_token, ncMeta = Noco.ncMeta) { - return await ncMeta.metaGet2(null, null, MetaTable.USERS, { + const userRefreshToken = await UserRefreshToken.getByToken( refresh_token, - }); + ncMeta, + ); + + if (!userRefreshToken) { + return null; + } + + return await ncMeta.metaGet2( + null, + null, + MetaTable.USERS, + userRefreshToken.fk_user_id, + ); } public static async list( @@ -262,6 +271,7 @@ export default class User implements UserType { args: { user?: User; baseId?: string; + orgId?: string; }, ncMeta = Noco.ncMeta, ) { diff --git a/packages/nocodb/src/models/UserRefreshToken.ts b/packages/nocodb/src/models/UserRefreshToken.ts new file mode 100644 index 0000000000..5bd644d6bd --- /dev/null +++ b/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, + 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 { + const userToken = await ncMeta.metaGet2( + null, + null, + MetaTable.USER_REFRESH_TOKENS, + { + token, + }, + ); + + if (!userToken) return null; + + userToken.meta = parseMetaProp(userToken); + + return userToken; + } +} diff --git a/packages/nocodb/src/models/index.ts b/packages/nocodb/src/models/index.ts index 2e62d9a533..ab22cdbdf1 100644 --- a/packages/nocodb/src/models/index.ts +++ b/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 Notification } from './Notification'; export { default as PresignedUrl } from './PresignedUrl'; +export { default as UserRefreshToken } from './UserRefreshToken'; diff --git a/packages/nocodb/src/services/users/users.service.ts b/packages/nocodb/src/services/users/users.service.ts index e3817c078f..c95fc3e565 100644 --- a/packages/nocodb/src/services/users/users.service.ts +++ b/packages/nocodb/src/services/users/users.service.ts @@ -21,7 +21,7 @@ import { validatePayload } from '~/helpers'; import { MetaService } from '~/meta/meta.service'; import { MetaTable } from '~/utils/globals'; import Noco from '~/Noco'; -import { Store, User } from '~/models'; +import { Store, User, UserRefreshToken } from '~/models'; import { randomTokenString } from '~/helpers/stringHelpers'; import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2'; import { NcError } from '~/helpers/catchError'; @@ -380,9 +380,9 @@ export class UsersService { const refreshToken = randomTokenString(); - await User.update(user.id, { - email: user.email, - refresh_token: refreshToken, + await UserRefreshToken.insert({ + token: refreshToken, + fk_user_id: user.id, }); setTokenCookie(param.res, refreshToken); @@ -495,9 +495,9 @@ export class UsersService { const refreshToken = randomTokenString(); - await User.update(user.id, { - refresh_token: refreshToken, - email: user.email, + await UserRefreshToken.insert({ + token: refreshToken, + fk_user_id: user.id, }); setTokenCookie(param.res, refreshToken); @@ -532,9 +532,10 @@ export class UsersService { const user = (param.req as any).user; if (user?.id) { await User.update(user.id, { - refresh_token: null, 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' }; } catch (e) { @@ -572,10 +573,15 @@ export class UsersService { } await User.update(user.id, { - refresh_token: refreshToken, - email: user.email, token_version: user['token_version'], }); + + await UserRefreshToken.insert({ + token: refreshToken, + fk_user_id: user.id, + meta: req.user?.extra, + }); + setTokenCookie(res, refreshToken); } } diff --git a/packages/nocodb/src/utils/globals.ts b/packages/nocodb/src/utils/globals.ts index 039b99155c..ff286fa015 100644 --- a/packages/nocodb/src/utils/globals.ts +++ b/packages/nocodb/src/utils/globals.ts @@ -29,7 +29,7 @@ export enum MetaTable { KANBAN_VIEW = 'nc_kanban_view_v2', KANBAN_VIEW_COLUMNS = 'nc_kanban_view_columns_v2', USERS = 'nc_users_v2', - ORGS = 'nc_orgs_v2', + ORGS_OLD = 'nc_orgs_v2', TEAMS = 'nc_teams_v2', TEAM_USERS = 'nc_team_users_v2', VIEWS = 'nc_views_v2', @@ -46,6 +46,7 @@ export enum MetaTable { MAP_VIEW_COLUMNS = 'nc_map_view_columns_v2', STORE = 'nc_store', NOTIFICATION = 'notification', + USER_REFRESH_TOKENS = 'nc_user_refresh_tokens', } export enum MetaTableOldV2 { @@ -60,7 +61,7 @@ export const orderedMetaTables = [ MetaTable.AUDIT, MetaTable.TEAM_USERS, MetaTable.TEAMS, - MetaTable.ORGS, + MetaTable.ORGS_OLD, MetaTable.PROJECT_USERS, MetaTable.USERS, MetaTable.MAP_VIEW, @@ -151,7 +152,7 @@ export enum CacheScope { MAP_VIEW_COLUMN = 'mapViewColumn', KANBAN_VIEW_COLUMN = 'kanbanViewColumn', USER = 'user', - ORGS = 'orgs', + ORGS_OLD = 'orgs', TEAM = 'team', TEAM_USER = 'teamUser', VIEW = 'view', diff --git a/packages/nocodb/src/utils/sanitiseUserObj.ts b/packages/nocodb/src/utils/sanitiseUserObj.ts index 01acdb2235..0ef1edcff6 100644 --- a/packages/nocodb/src/utils/sanitiseUserObj.ts +++ b/packages/nocodb/src/utils/sanitiseUserObj.ts @@ -1,7 +1,6 @@ const ignoreKeys = new Set([ 'password', 'salt', - 'refresh_token', 'invite_token', 'invite_token_expires', 'reset_password_expires',