Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import sequelize, {
2 type CreationOptional,
3 type HasManyGetAssociationsMixin,
4 type InferAttributes,
5 type InferCreationAttributes,
6 type Sequelize,
7} from 'sequelize';
8
9import { UserPenaltySeverity } from '../services/moderationConfigService/index.js';
10import { validateUrl } from '../utils/url.js';
11import { type LocationBank } from './banks/LocationBankModel.js';
12import { type DataTypes } from './index.js';
13import { type Policy } from './PolicyModel.js';
14import { type SequelizeAction } from './rules/ActionModel.js';
15import { type Rule } from './rules/RuleModel.js';
16import { type User } from './UserModel.js';
17
18const { Model } = sequelize;
19
20export type Org = InstanceType<ReturnType<typeof makeOrgModel>>;
21export type PolicyActionPenalties = {
22 actionId: string;
23 policyId: string;
24 penalties: number[];
25};
26
27/**
28 * Data Model for Organizations
29 */
30export default function makeOrgModel(
31 sequelize: Sequelize,
32 DataTypes: DataTypes,
33) {
34 class Org extends Model<
35 InferAttributes<Org, { omit: 'createdAt' | 'updatedAt' }>,
36 InferCreationAttributes<Org, { omit: 'createdAt' | 'updatedAt' }>
37 > {
38 public declare id: string;
39 public declare email: string;
40 public declare name: string;
41 public declare websiteUrl: string;
42 public declare apiKeyId?: CreationOptional<string>;
43 public declare onCallAlertEmail?: CreationOptional<string>;
44
45 public declare getRules: HasManyGetAssociationsMixin<Rule>;
46 public declare getActions: HasManyGetAssociationsMixin<SequelizeAction>;
47 // Has to use any below to avoid circular type errors.
48 // eslint-disable-next-line @typescript-eslint/no-explicit-any
49 public declare getContentTypes: HasManyGetAssociationsMixin<any>;
50 public declare getLocationBanks: HasManyGetAssociationsMixin<LocationBank>;
51 public declare getUsers: HasManyGetAssociationsMixin<User>;
52 public declare getPolicies: HasManyGetAssociationsMixin<Policy>;
53 public declare createdAt: Date;
54 public declare updatedAt: Date;
55
56 // eslint-disable-next-line @typescript-eslint/no-explicit-any
57 static associate(models: { [key: string]: any }) {
58 Org.hasMany(models.User, { as: 'Users' });
59 Org.hasMany(models.Rule, { as: 'Rules' });
60 Org.hasMany(models.Action, { as: 'Actions', foreignKey: 'orgId' });
61 Org.hasMany(models.ItemType, { as: 'ContentTypes' });
62 Org.hasMany(models.LocationBank, { as: 'LocationBanks' });
63 Org.hasMany(models.Policy, { as: 'policies' });
64 }
65
66 static async getPolicyActionPenaltiesEventuallyConsistent(orgId: string) {
67 const [actions, policies] = await Promise.all([
68 sequelize.models.Action.findAll({ where: { orgId } }),
69 sequelize.models.Policy.findAll({ where: { orgId } }),
70 ]);
71
72 return (policies as Policy[]).flatMap((policy) =>
73 (actions as SequelizeAction[]).map(
74 (action): PolicyActionPenalties => ({
75 actionId: action.id,
76 policyId: policy.id,
77 penalties: [
78 computeActionPolicyPenalty(action.penalty, policy.penalty),
79 ],
80 }),
81 ),
82 );
83 }
84 }
85
86 /* Fields */
87 Org.init(
88 {
89 id: {
90 type: DataTypes.STRING,
91 primaryKey: true,
92 },
93 email: {
94 type: DataTypes.STRING,
95 unique: true,
96 allowNull: false,
97 validate: {
98 isEmail: true,
99 notEmpty: true,
100 },
101 },
102 name: {
103 type: DataTypes.STRING,
104 unique: true,
105 allowNull: false,
106 validate: {
107 notEmpty: true,
108 },
109 },
110 websiteUrl: {
111 type: DataTypes.STRING,
112 unique: true,
113 allowNull: false,
114 validate: {
115 isValidUrl: validateUrl,
116 },
117 },
118 // ID of the AWS API Key resource that stores the API key. Not actually
119 // used for anything at the moment (instead, the API key is looked up in
120 // but potentially useful.
121 apiKeyId: {
122 type: DataTypes.STRING,
123 },
124 onCallAlertEmail: {
125 type: DataTypes.STRING,
126 validate: {
127 isEmail: true,
128 },
129 },
130 },
131 {
132 sequelize,
133 modelName: 'org',
134 underscored: true,
135 },
136 );
137
138 return Org;
139}
140
141/**
142 * Computes the severity of the penalty we should apply for a given
143 * (action, policy) pair. The general idea is to make the penalties
144 * increase exponentially as severity levels increase, but the rate
145 * of increase can't be so high that a (severe, severe) penalty is
146 * 50x higher than a (high, high) penalty.
147 *
148 * The easiest way to achieve this exponential behavior is at the individual
149 * severity levels, rather than trying to multiply the action penalty
150 * by the severity penalty to compound their magnitudes. So the severity
151 * levels apply penalty magnitudes as follows:
152 *
153 * NONE = 0
154 * LOW = 1
155 * MEDIUM = 3
156 * HIGH = 9
157 * SEVERE = 27
158 *
159 * To get the penalty value for an (action, policy) pair, we just add the
160 * penalty values of the action and penalty because the exponential nature
161 * of these penalties has already been taken into account.
162 */
163function computeActionPolicyPenalty(
164 actionPenalty: UserPenaltySeverity,
165 policyPenalty: UserPenaltySeverity,
166) {
167 // Type annotation makes sure that every possible severity has a score.
168 const penaltySeverityMap: { [k in UserPenaltySeverity]: number } = {
169 [UserPenaltySeverity.NONE]: 0,
170 [UserPenaltySeverity.LOW]: 1,
171 [UserPenaltySeverity.MEDIUM]: 3,
172 [UserPenaltySeverity.HIGH]: 9,
173 [UserPenaltySeverity.SEVERE]: 27,
174 };
175
176 // If the action has no penalty (e.g., "Send to Moderation", "Restore
177 // Content"), we never apply any penalty, regardless of the policy penalty.
178 // Otherwise, the penalty accounts for both the action + policy penalties.
179 return actionPenalty === UserPenaltySeverity.NONE
180 ? 0
181 : penaltySeverityMap[actionPenalty] + penaltySeverityMap[policyPenalty];
182}