feat: introduce role based access control
This commit is contained in:
@@ -40,6 +40,9 @@ class Connection extends Base {
|
||||
userId: { type: 'string', format: 'uuid' },
|
||||
verified: { type: 'boolean', default: false },
|
||||
draft: { type: 'boolean' },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -31,6 +31,9 @@ class ExecutionStep extends Base {
|
||||
dataOut: { type: ['object', 'null'] },
|
||||
status: { type: 'string', enum: ['success', 'failure'] },
|
||||
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' },
|
||||
testRun: { type: 'boolean', default: false },
|
||||
internalId: { type: 'string' },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -19,7 +19,7 @@ class Flow extends Base {
|
||||
status: 'paused' | 'published' | 'draft';
|
||||
steps: Step[];
|
||||
triggerStep: Step;
|
||||
published_at: string;
|
||||
publishedAt: string;
|
||||
remoteWebhookId: string;
|
||||
executions?: Execution[];
|
||||
lastExecution?: Execution;
|
||||
@@ -37,6 +37,10 @@ class Flow extends Base {
|
||||
userId: { type: 'string', format: 'uuid' },
|
||||
remoteWebhookId: { type: 'string' },
|
||||
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
|
||||
);
|
||||
qb.onBuild((builder) => {
|
||||
if (!builder.context().withSoftDeleted) {
|
||||
if (!builder.context().withSoftDeleted && qb.modelClass().jsonSchema.properties.deletedAt) {
|
||||
builder.whereNull(
|
||||
`${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' },
|
||||
parameters: { type: 'object' },
|
||||
webhookPath: { type: ['string', 'null'] },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -46,6 +46,9 @@ class Subscription extends Base {
|
||||
nextBillDate: { type: 'string' },
|
||||
lastBillDate: { type: 'string' },
|
||||
cancellationEffectiveDate: { type: 'string' },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -84,7 +87,7 @@ class Subscription extends Base {
|
||||
return (
|
||||
this.status === 'deleted' &&
|
||||
Number(this.cancellationEffectiveDate) >
|
||||
DateTime.now().startOf('day').toMillis()
|
||||
DateTime.now().startOf('day').toMillis()
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -24,6 +24,9 @@ class UsageData extends Base {
|
||||
subscriptionId: { type: 'string', format: 'uuid' },
|
||||
consumedTaskCount: { type: 'integer' },
|
||||
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 bcrypt from 'bcrypt';
|
||||
import crypto from 'crypto';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Ability } from '@casl/ability';
|
||||
import type { Subject } from '@casl/ability';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import Base from './base';
|
||||
import ExtendedQueryBuilder from './query-builder';
|
||||
import Connection from './connection';
|
||||
import Flow from './flow';
|
||||
import Step from './step';
|
||||
import Role from './role';
|
||||
import Permission from './permission';
|
||||
import Execution from './execution';
|
||||
import UsageData from './usage-data.ee';
|
||||
import Subscription from './subscription.ee';
|
||||
@@ -16,8 +21,8 @@ class User extends Base {
|
||||
id!: string;
|
||||
fullName!: string;
|
||||
email!: string;
|
||||
roleId: string;
|
||||
password!: string;
|
||||
role: string;
|
||||
resetPasswordToken: string;
|
||||
resetPasswordTokenSentAt: string;
|
||||
trialExpiryDate: string;
|
||||
@@ -29,6 +34,8 @@ class User extends Base {
|
||||
currentUsageData?: UsageData;
|
||||
subscriptions?: Subscription[];
|
||||
currentSubscription?: Subscription;
|
||||
role: Role;
|
||||
permissions: Permission[];
|
||||
|
||||
static tableName = 'users';
|
||||
|
||||
@@ -41,7 +48,13 @@ class User extends Base {
|
||||
fullName: { type: 'string', minLength: 1 },
|
||||
email: { type: 'string', format: 'email', 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();
|
||||
},
|
||||
},
|
||||
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) {
|
||||
@@ -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;
|
||||
|
Reference in New Issue
Block a user