feat: introduce role based access control
This commit is contained in:
@@ -2,18 +2,55 @@ import appConfig from '../../src/config/app';
|
|||||||
import logger from '../../src/helpers/logger';
|
import logger from '../../src/helpers/logger';
|
||||||
import client from './client';
|
import client from './client';
|
||||||
import User from '../../src/models/user';
|
import User from '../../src/models/user';
|
||||||
|
import Role from '../../src/models/role';
|
||||||
|
import Permission from '../../src/models/permission';
|
||||||
import '../../src/config/orm';
|
import '../../src/config/orm';
|
||||||
|
|
||||||
|
async function seedPermissionsIfNeeded() {
|
||||||
|
const existingPermissions = await Permission.query().limit(1).first();
|
||||||
|
|
||||||
|
if (!existingPermissions) return;
|
||||||
|
|
||||||
|
const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
|
||||||
|
|
||||||
|
await Permission.query().insert([
|
||||||
|
...getPermission('Connection', ['create', 'read', 'delete', 'update']),
|
||||||
|
...getPermission('Execution', ['read']),
|
||||||
|
...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']),
|
||||||
|
...getPermission('Role', ['create', 'delete', 'read', 'update']),
|
||||||
|
...getPermission('User', ['create', 'delete', 'read', 'update']),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOrFetchRole() {
|
||||||
|
const role = await Role.query().limit(1).first();
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
const createdRole = await Role.query().insertAndFetch({
|
||||||
|
name: 'Admin',
|
||||||
|
key: 'admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
return createdRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createUser(
|
export async function createUser(
|
||||||
email = 'user@automatisch.io',
|
email = 'user@automatisch.io',
|
||||||
password = 'sample'
|
password = 'sample'
|
||||||
) {
|
) {
|
||||||
const UNIQUE_VIOLATION_CODE = '23505';
|
const UNIQUE_VIOLATION_CODE = '23505';
|
||||||
|
|
||||||
|
await seedPermissionsIfNeeded();
|
||||||
|
|
||||||
|
const role = await createOrFetchRole();
|
||||||
const userParams = {
|
const userParams = {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
fullName: 'Initial admin',
|
fullName: 'Initial admin',
|
||||||
role: 'admin',
|
roleId: role.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -12,6 +12,7 @@ const knexConfig = {
|
|||||||
database: appConfig.postgresDatabase,
|
database: appConfig.postgresDatabase,
|
||||||
ssl: appConfig.postgresEnableSsl,
|
ssl: appConfig.postgresEnableSsl,
|
||||||
},
|
},
|
||||||
|
asyncStackTraces: appConfig.isDev,
|
||||||
searchPath: [appConfig.postgresSchema],
|
searchPath: [appConfig.postgresSchema],
|
||||||
pool: { min: 0, max: 20 },
|
pool: { min: 0, max: 20 },
|
||||||
migrations: {
|
migrations: {
|
||||||
|
@@ -24,6 +24,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@automatisch/web": "^0.7.1",
|
"@automatisch/web": "^0.7.1",
|
||||||
"@bull-board/express": "^3.10.1",
|
"@bull-board/express": "^3.10.1",
|
||||||
|
"@casl/ability": "^6.5.0",
|
||||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||||
"@graphql-tools/load": "^7.5.2",
|
"@graphql-tools/load": "^7.5.2",
|
||||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||||
|
@@ -0,0 +1,32 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import capitalize from 'lodash/capitalize';
|
||||||
|
import lowerCase from 'lodash/lowerCase';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.createTable('roles', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table.string('name').notNullable();
|
||||||
|
table.string('key').notNullable();
|
||||||
|
table.string('description');
|
||||||
|
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueUserRoles = await knex('users')
|
||||||
|
.select('role')
|
||||||
|
.groupBy('role');
|
||||||
|
|
||||||
|
for (const { role } of uniqueUserRoles) {
|
||||||
|
// skip empty roles
|
||||||
|
if (!role) continue;
|
||||||
|
|
||||||
|
await knex('roles').insert({
|
||||||
|
name: capitalize(role),
|
||||||
|
key: lowerCase(role),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.dropTable('roles');
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.createTable('permissions', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table.string('action').notNullable();
|
||||||
|
table.string('subject').notNullable();
|
||||||
|
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex('permissions').insert([
|
||||||
|
...getPermission('Connection', ['create', 'read', 'delete', 'update']),
|
||||||
|
...getPermission('Execution', ['read']),
|
||||||
|
...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']),
|
||||||
|
...getPermission('Role', ['create', 'delete', 'read', 'update']),
|
||||||
|
...getPermission('User', ['create', 'delete', 'read', 'update']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.dropTable('permissions');
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.createTable('roles_permissions', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table.uuid('role_id').references('id').inTable('roles');
|
||||||
|
table.uuid('permission_id').references('id').inTable('permissions');
|
||||||
|
});
|
||||||
|
|
||||||
|
const roles = await knex('roles').select('id');
|
||||||
|
const permissions = await knex('permissions').select('id');
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
for (const permission of permissions) {
|
||||||
|
await knex('roles_permissions').insert({
|
||||||
|
role_id: role.id,
|
||||||
|
permission_id: permission.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return knex.schema.dropTable('roles_permissions');
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.table('users', async (table) => {
|
||||||
|
table.uuid('role_id').references('id').inTable('roles');
|
||||||
|
});
|
||||||
|
|
||||||
|
const theRole = await knex('roles').select('id').limit(1).first();
|
||||||
|
const roles = await knex('roles').select('id', 'key');
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
await knex('users')
|
||||||
|
.where({
|
||||||
|
role: role.key
|
||||||
|
})
|
||||||
|
.update({
|
||||||
|
role_id: role.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// backfill not-migratables
|
||||||
|
await knex('users').whereNull('role_id').update({ role_id: theRole.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return await knex.schema.table('users', (table) => {
|
||||||
|
table.dropColumn('role_id');
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.table('users', async (table) => {
|
||||||
|
table.dropColumn('role');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
return await knex.schema.table('users', (table) => {
|
||||||
|
table.string('role').defaultTo('user');
|
||||||
|
});
|
||||||
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
import User from '../../models/user';
|
import User from '../../models/user';
|
||||||
|
import Role from '../../models/role';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
input: {
|
input: {
|
||||||
@@ -17,11 +18,13 @@ const createUser = async (_parent: unknown, params: Params) => {
|
|||||||
throw new Error('User already exists!');
|
throw new Error('User already exists!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const role = await Role.query().findOne({ key: 'user' });
|
||||||
|
|
||||||
const user = await User.query().insert({
|
const user = await User.query().insert({
|
||||||
fullName,
|
fullName,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
role: 'user',
|
roleId: role.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
@@ -18,6 +18,8 @@ const updateFlowStatus = async (
|
|||||||
params: Params,
|
params: Params,
|
||||||
context: Context
|
context: Context
|
||||||
) => {
|
) => {
|
||||||
|
context.currentUser.can('publish', 'Flow');
|
||||||
|
|
||||||
let flow = await context.currentUser
|
let flow = await context.currentUser
|
||||||
.$relatedQuery('flows')
|
.$relatedQuery('flows')
|
||||||
.findOne({
|
.findOne({
|
||||||
@@ -55,7 +57,7 @@ const updateFlowStatus = async (
|
|||||||
} else {
|
} else {
|
||||||
if (newActiveValue) {
|
if (newActiveValue) {
|
||||||
flow = await flow.$query().patchAndFetch({
|
flow = await flow.$query().patchAndFetch({
|
||||||
published_at: new Date().toISOString(),
|
publishedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const jobName = `${JOB_NAME}-${flow.id}`;
|
const jobName = `${JOB_NAME}-${flow.id}`;
|
||||||
@@ -78,7 +80,10 @@ const updateFlowStatus = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flow = await flow.$query().withGraphFetched('steps').patchAndFetch({
|
flow = await flow
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched('steps')
|
||||||
|
.patchAndFetch({
|
||||||
active: newActiveValue,
|
active: newActiveValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -10,6 +10,8 @@ type Params = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
|
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
|
||||||
|
context.currentUser.can('read', 'Flow');
|
||||||
|
|
||||||
const flowsQuery = context.currentUser
|
const flowsQuery = context.currentUser
|
||||||
.$relatedQuery('flows')
|
.$relatedQuery('flows')
|
||||||
.joinRelated({
|
.joinRelated({
|
||||||
|
@@ -12,7 +12,15 @@ const isAuthenticated = rule()(async (_parent, _args, req) => {
|
|||||||
const { userId } = jwt.verify(token, appConfig.appSecretKey) as {
|
const { userId } = jwt.verify(token, appConfig.appSecretKey) as {
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
req.currentUser = await User.query().findById(userId).throwIfNotFound();
|
req.currentUser = await User
|
||||||
|
.query()
|
||||||
|
.findById(userId)
|
||||||
|
.joinRelated({
|
||||||
|
permissions: true,
|
||||||
|
})
|
||||||
|
.withGraphFetched({
|
||||||
|
permissions: true,
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
import ExtendedQueryBuilder from '../models/query-builder';
|
import ExtendedQueryBuilder from '../models/query-builder';
|
||||||
|
import type Base from '../models/base';
|
||||||
|
|
||||||
const paginate = async (
|
const paginate = async (
|
||||||
query: ExtendedQueryBuilder<Model, Model[]>,
|
query: ExtendedQueryBuilder<Model, Model[]>,
|
||||||
limit: number,
|
limit: number,
|
||||||
offset: number
|
offset: number,
|
||||||
) => {
|
) => {
|
||||||
if (limit < 1 || limit > 100) {
|
if (limit < 1 || limit > 100) {
|
||||||
throw new Error('Limit must be between 1 and 100');
|
throw new Error('Limit must be between 1 and 100');
|
||||||
@@ -20,11 +21,9 @@ const paginate = async (
|
|||||||
currentPage: Math.ceil(offset / limit + 1),
|
currentPage: Math.ceil(offset / limit + 1),
|
||||||
totalPages: Math.ceil(count / limit),
|
totalPages: Math.ceil(count / limit),
|
||||||
},
|
},
|
||||||
edges: records.map((record: Model) => {
|
edges: records.map((record: Base) => ({
|
||||||
return {
|
|
||||||
node: record,
|
node: record,
|
||||||
};
|
})),
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -40,6 +40,9 @@ class Connection extends Base {
|
|||||||
userId: { type: 'string', format: 'uuid' },
|
userId: { type: 'string', format: 'uuid' },
|
||||||
verified: { type: 'boolean', default: false },
|
verified: { type: 'boolean', default: false },
|
||||||
draft: { type: 'boolean' },
|
draft: { type: 'boolean' },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -31,6 +31,9 @@ class ExecutionStep extends Base {
|
|||||||
dataOut: { type: ['object', 'null'] },
|
dataOut: { type: ['object', 'null'] },
|
||||||
status: { type: 'string', enum: ['success', 'failure'] },
|
status: { type: 'string', enum: ['success', 'failure'] },
|
||||||
errorDetails: { type: ['object', 'null'] },
|
errorDetails: { type: ['object', 'null'] },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -22,6 +22,9 @@ class Execution extends Base {
|
|||||||
flowId: { type: 'string', format: 'uuid' },
|
flowId: { type: 'string', format: 'uuid' },
|
||||||
testRun: { type: 'boolean', default: false },
|
testRun: { type: 'boolean', default: false },
|
||||||
internalId: { type: 'string' },
|
internalId: { type: 'string' },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -19,7 +19,7 @@ class Flow extends Base {
|
|||||||
status: 'paused' | 'published' | 'draft';
|
status: 'paused' | 'published' | 'draft';
|
||||||
steps: Step[];
|
steps: Step[];
|
||||||
triggerStep: Step;
|
triggerStep: Step;
|
||||||
published_at: string;
|
publishedAt: string;
|
||||||
remoteWebhookId: string;
|
remoteWebhookId: string;
|
||||||
executions?: Execution[];
|
executions?: Execution[];
|
||||||
lastExecution?: Execution;
|
lastExecution?: Execution;
|
||||||
@@ -37,6 +37,10 @@ class Flow extends Base {
|
|||||||
userId: { type: 'string', format: 'uuid' },
|
userId: { type: 'string', format: 'uuid' },
|
||||||
remoteWebhookId: { type: 'string' },
|
remoteWebhookId: { type: 'string' },
|
||||||
active: { type: 'boolean' },
|
active: { type: 'boolean' },
|
||||||
|
publishedAt: { type: 'string' },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
24
packages/backend/src/models/permission.ts
Normal file
24
packages/backend/src/models/permission.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Base from './base';
|
||||||
|
|
||||||
|
class Permission extends Base {
|
||||||
|
id: string;
|
||||||
|
action: string;
|
||||||
|
subject: string;
|
||||||
|
|
||||||
|
static tableName = 'permissions';
|
||||||
|
|
||||||
|
static jsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['action', 'subject'],
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', format: 'uuid' },
|
||||||
|
action: { type: 'string', minLength: 1 },
|
||||||
|
subject: { type: 'string', minLength: 1 },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Permission;
|
@@ -15,7 +15,7 @@ const buildQueryBuidlerForClass = (): ForClassMethod => {
|
|||||||
modelClass
|
modelClass
|
||||||
);
|
);
|
||||||
qb.onBuild((builder) => {
|
qb.onBuild((builder) => {
|
||||||
if (!builder.context().withSoftDeleted) {
|
if (!builder.context().withSoftDeleted && qb.modelClass().jsonSchema.properties.deletedAt) {
|
||||||
builder.whereNull(
|
builder.whereNull(
|
||||||
`${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}`
|
`${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}`
|
||||||
);
|
);
|
||||||
|
39
packages/backend/src/models/role.ts
Normal file
39
packages/backend/src/models/role.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import Base from './base';
|
||||||
|
import User from './user';
|
||||||
|
|
||||||
|
class Role extends Base {
|
||||||
|
id!: string;
|
||||||
|
name!: string;
|
||||||
|
key: string;
|
||||||
|
description: string;
|
||||||
|
users?: User[];
|
||||||
|
|
||||||
|
static tableName = 'roles';
|
||||||
|
|
||||||
|
static jsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['name', 'key'],
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', format: 'uuid' },
|
||||||
|
name: { type: 'string', minLength: 1 },
|
||||||
|
key: { type: 'string', minLength: 1 },
|
||||||
|
description: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static relationMappings = () => ({
|
||||||
|
users: {
|
||||||
|
relation: Base.HasManyRelation,
|
||||||
|
modelClass: User,
|
||||||
|
join: {
|
||||||
|
from: 'roles.id',
|
||||||
|
to: 'users.role_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Role;
|
@@ -46,6 +46,9 @@ class Step extends Base {
|
|||||||
position: { type: 'integer' },
|
position: { type: 'integer' },
|
||||||
parameters: { type: 'object' },
|
parameters: { type: 'object' },
|
||||||
webhookPath: { type: ['string', 'null'] },
|
webhookPath: { type: ['string', 'null'] },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -46,6 +46,9 @@ class Subscription extends Base {
|
|||||||
nextBillDate: { type: 'string' },
|
nextBillDate: { type: 'string' },
|
||||||
lastBillDate: { type: 'string' },
|
lastBillDate: { type: 'string' },
|
||||||
cancellationEffectiveDate: { type: 'string' },
|
cancellationEffectiveDate: { type: 'string' },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -24,6 +24,9 @@ class UsageData extends Base {
|
|||||||
subscriptionId: { type: 'string', format: 'uuid' },
|
subscriptionId: { type: 'string', format: 'uuid' },
|
||||||
consumedTaskCount: { type: 'integer' },
|
consumedTaskCount: { type: 'integer' },
|
||||||
nextResetAt: { type: 'string' },
|
nextResetAt: { type: 'string' },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,13 +1,18 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
import { QueryContext, ModelOptions } from 'objection';
|
import { QueryContext, ModelOptions } from 'objection';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import crypto from 'crypto';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { Ability } from '@casl/ability';
|
||||||
|
import type { Subject } from '@casl/ability';
|
||||||
|
|
||||||
import appConfig from '../config/app';
|
import appConfig from '../config/app';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import ExtendedQueryBuilder from './query-builder';
|
import ExtendedQueryBuilder from './query-builder';
|
||||||
import Connection from './connection';
|
import Connection from './connection';
|
||||||
import Flow from './flow';
|
import Flow from './flow';
|
||||||
import Step from './step';
|
import Step from './step';
|
||||||
|
import Role from './role';
|
||||||
|
import Permission from './permission';
|
||||||
import Execution from './execution';
|
import Execution from './execution';
|
||||||
import UsageData from './usage-data.ee';
|
import UsageData from './usage-data.ee';
|
||||||
import Subscription from './subscription.ee';
|
import Subscription from './subscription.ee';
|
||||||
@@ -16,8 +21,8 @@ class User extends Base {
|
|||||||
id!: string;
|
id!: string;
|
||||||
fullName!: string;
|
fullName!: string;
|
||||||
email!: string;
|
email!: string;
|
||||||
|
roleId: string;
|
||||||
password!: string;
|
password!: string;
|
||||||
role: string;
|
|
||||||
resetPasswordToken: string;
|
resetPasswordToken: string;
|
||||||
resetPasswordTokenSentAt: string;
|
resetPasswordTokenSentAt: string;
|
||||||
trialExpiryDate: string;
|
trialExpiryDate: string;
|
||||||
@@ -29,6 +34,8 @@ class User extends Base {
|
|||||||
currentUsageData?: UsageData;
|
currentUsageData?: UsageData;
|
||||||
subscriptions?: Subscription[];
|
subscriptions?: Subscription[];
|
||||||
currentSubscription?: Subscription;
|
currentSubscription?: Subscription;
|
||||||
|
role: Role;
|
||||||
|
permissions: Permission[];
|
||||||
|
|
||||||
static tableName = 'users';
|
static tableName = 'users';
|
||||||
|
|
||||||
@@ -41,7 +48,13 @@ class User extends Base {
|
|||||||
fullName: { type: 'string', minLength: 1 },
|
fullName: { type: 'string', minLength: 1 },
|
||||||
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
|
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
|
||||||
password: { type: 'string', minLength: 1, maxLength: 255 },
|
password: { type: 'string', minLength: 1, maxLength: 255 },
|
||||||
role: { type: 'string', enum: ['admin', 'user'] },
|
resetPasswordToken: { type: 'string' },
|
||||||
|
resetPasswordTokenSentAt: { type: 'string' },
|
||||||
|
trialExpiryDate: { type: 'string' },
|
||||||
|
roleId: { type: 'string', format: 'uuid' },
|
||||||
|
deletedAt: { type: 'string' },
|
||||||
|
createdAt: { type: 'string' },
|
||||||
|
updatedAt: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,6 +137,26 @@ class User extends Base {
|
|||||||
builder.orderBy('created_at', 'desc').limit(1).first();
|
builder.orderBy('created_at', 'desc').limit(1).first();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
role: {
|
||||||
|
relation: Base.HasOneRelation,
|
||||||
|
modelClass: Role,
|
||||||
|
join: {
|
||||||
|
from: 'roles.id',
|
||||||
|
to: 'users.role_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
relation: Base.ManyToManyRelation,
|
||||||
|
modelClass: Permission,
|
||||||
|
join: {
|
||||||
|
from: 'users.role_id',
|
||||||
|
through: {
|
||||||
|
from: 'roles_permissions.role_id',
|
||||||
|
to: 'roles_permissions.permission_id',
|
||||||
|
},
|
||||||
|
to: 'permissions.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
login(password: string) {
|
login(password: string) {
|
||||||
@@ -248,6 +281,30 @@ class User extends Base {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get ability() {
|
||||||
|
if (!this.permissions) {
|
||||||
|
throw new Error('User.permissions must be fetched!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Ability(this.permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
can(action: string, subject: Subject) {
|
||||||
|
const can = this.ability.can(action, subject);
|
||||||
|
|
||||||
|
if (!can) throw new Error('Not authorized!');
|
||||||
|
|
||||||
|
return can;
|
||||||
|
}
|
||||||
|
|
||||||
|
cannot(action: string, subject: Subject) {
|
||||||
|
const cannot = this.ability.cannot(action, subject);
|
||||||
|
|
||||||
|
if (cannot) throw new Error('Not authorized!');
|
||||||
|
|
||||||
|
return cannot;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
35
yarn.lock
35
yarn.lock
@@ -1428,6 +1428,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@bull-board/api" "3.10.1"
|
"@bull-board/api" "3.10.1"
|
||||||
|
|
||||||
|
"@casl/ability@^6.5.0":
|
||||||
|
version "6.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@casl/ability/-/ability-6.5.0.tgz#a151a7637886099b8ffe52a96601225004a5c157"
|
||||||
|
integrity sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw==
|
||||||
|
dependencies:
|
||||||
|
"@ucast/mongo2js" "^1.3.0"
|
||||||
|
|
||||||
"@colors/colors@1.5.0":
|
"@colors/colors@1.5.0":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||||
@@ -4528,6 +4535,34 @@
|
|||||||
"@typescript-eslint/types" "5.10.0"
|
"@typescript-eslint/types" "5.10.0"
|
||||||
eslint-visitor-keys "^3.0.0"
|
eslint-visitor-keys "^3.0.0"
|
||||||
|
|
||||||
|
"@ucast/core@^1.0.0", "@ucast/core@^1.4.1", "@ucast/core@^1.6.1":
|
||||||
|
version "1.10.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@ucast/core/-/core-1.10.2.tgz#30b6b893479823265368e528b61b042f752f2c92"
|
||||||
|
integrity sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==
|
||||||
|
|
||||||
|
"@ucast/js@^3.0.0":
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@ucast/js/-/js-3.0.3.tgz#6ff618a85bd95f1a8f46658cc663a1f798de327f"
|
||||||
|
integrity sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ==
|
||||||
|
dependencies:
|
||||||
|
"@ucast/core" "^1.0.0"
|
||||||
|
|
||||||
|
"@ucast/mongo2js@^1.3.0":
|
||||||
|
version "1.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@ucast/mongo2js/-/mongo2js-1.3.4.tgz#579f9e5eb074cba54640d5c70c71c500580f3af3"
|
||||||
|
integrity sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA==
|
||||||
|
dependencies:
|
||||||
|
"@ucast/core" "^1.6.1"
|
||||||
|
"@ucast/js" "^3.0.0"
|
||||||
|
"@ucast/mongo" "^2.4.0"
|
||||||
|
|
||||||
|
"@ucast/mongo@^2.4.0":
|
||||||
|
version "2.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@ucast/mongo/-/mongo-2.4.3.tgz#92b1dd7c0ab06a907f2ab1422aa3027518ccc05e"
|
||||||
|
integrity sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==
|
||||||
|
dependencies:
|
||||||
|
"@ucast/core" "^1.4.1"
|
||||||
|
|
||||||
"@vitejs/plugin-vue@^3.1.2":
|
"@vitejs/plugin-vue@^3.1.2":
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.1.2.tgz#3cd52114e8871a0b5e7bd7d837469c032e503036"
|
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.1.2.tgz#3cd52114e8871a0b5e7bd7d837469c032e503036"
|
||||||
|
Reference in New Issue
Block a user