mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 10:55:37 +00:00
Merge branch 'main' into features/deits
This commit is contained in:
commit
e10ac14726
@ -198,38 +198,73 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle databases that don't support window function ROW_NUMBER (here it's MySQL 5)
|
||||
if (!strapi.db.dialect.supportsWindowFunctions()) {
|
||||
await cleanOrderColumnsForOldDatabases({ id, attribute, db, inverseRelIds, transaction: trx });
|
||||
return;
|
||||
}
|
||||
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
||||
const update = [];
|
||||
const updateBinding = [];
|
||||
const select = ['??'];
|
||||
const selectBinding = ['id'];
|
||||
const where = [];
|
||||
const whereBinding = [];
|
||||
|
||||
if (hasOrderColumn(attribute) && id) {
|
||||
update.push('?? = b.src_order');
|
||||
updateBinding.push(orderColumnName);
|
||||
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
|
||||
selectBinding.push(joinColumn.name, orderColumnName);
|
||||
where.push('?? = ?');
|
||||
whereBinding.push(joinColumn.name, id);
|
||||
}
|
||||
|
||||
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
||||
update.push('?? = b.inv_order');
|
||||
updateBinding.push(inverseOrderColumnName);
|
||||
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
|
||||
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
|
||||
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
|
||||
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
|
||||
}
|
||||
|
||||
switch (strapi.db.dialect.client) {
|
||||
case 'mysql':
|
||||
await cleanOrderColumnsForInnoDB({ id, attribute, db, inverseRelIds, transaction: trx });
|
||||
// Here it's MariaDB and MySQL 8
|
||||
await db
|
||||
.getConnection()
|
||||
.raw(
|
||||
`UPDATE
|
||||
?? as a,
|
||||
(
|
||||
SELECT ${select.join(', ')}
|
||||
FROM ??
|
||||
WHERE ${where.join(' OR ')}
|
||||
) AS b
|
||||
SET ${update.join(', ')}
|
||||
WHERE b.id = a.id`,
|
||||
[joinTable.name, ...selectBinding, joinTable.name, ...whereBinding, ...updateBinding]
|
||||
)
|
||||
.transacting(trx);
|
||||
break;
|
||||
/*
|
||||
UPDATE
|
||||
:joinTable: as a,
|
||||
(
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
|
||||
FROM :joinTable:
|
||||
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
|
||||
) AS b
|
||||
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
|
||||
WHERE b.id = a.id;
|
||||
*/
|
||||
default: {
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
||||
const update = [];
|
||||
const updateBinding = [];
|
||||
const select = ['??'];
|
||||
const selectBinding = ['id'];
|
||||
const where = [];
|
||||
const whereBinding = [];
|
||||
|
||||
if (hasOrderColumn(attribute) && id) {
|
||||
update.push('?? = b.src_order');
|
||||
updateBinding.push(orderColumnName);
|
||||
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
|
||||
selectBinding.push(joinColumn.name, orderColumnName);
|
||||
where.push('?? = ?');
|
||||
whereBinding.push(joinColumn.name, id);
|
||||
}
|
||||
|
||||
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
||||
update.push('?? = b.inv_order');
|
||||
updateBinding.push(inverseOrderColumnName);
|
||||
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
|
||||
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
|
||||
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
|
||||
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
|
||||
}
|
||||
|
||||
const joinTableName = addSchema(joinTable.name);
|
||||
|
||||
// raw query as knex doesn't allow updating from a subquery
|
||||
@ -249,17 +284,17 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
|
||||
.transacting(trx);
|
||||
|
||||
/*
|
||||
`UPDATE :joinTable: as a
|
||||
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
|
||||
FROM (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
|
||||
FROM :joinTable:
|
||||
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
|
||||
) AS b
|
||||
WHERE b.id = a.id`,
|
||||
UPDATE :joinTable: as a
|
||||
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
|
||||
FROM (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
|
||||
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
|
||||
FROM :joinTable:
|
||||
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
|
||||
) AS b
|
||||
WHERE b.id = a.id;
|
||||
*/
|
||||
}
|
||||
}
|
||||
@ -267,9 +302,9 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
|
||||
|
||||
/*
|
||||
* Ensure that orders are following a 1, 2, 3 sequence, without gap.
|
||||
* The use of a temporary table instead of a window function makes the query compatible with MySQL 5 and prevents some deadlocks to happen in innoDB databases
|
||||
* The use of a session variable instead of a window function makes the query compatible with MySQL 5
|
||||
*/
|
||||
const cleanOrderColumnsForInnoDB = async ({
|
||||
const cleanOrderColumnsForOldDatabases = async ({
|
||||
id,
|
||||
attribute,
|
||||
db,
|
||||
@ -279,96 +314,68 @@ const cleanOrderColumnsForInnoDB = async ({
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
||||
|
||||
const now = new Date().valueOf();
|
||||
const randomHex = randomBytes(16).toString('hex');
|
||||
const randomSuffix = `${new Date().valueOf()}_${randomBytes(16).toString('hex')}`;
|
||||
|
||||
if (hasOrderColumn(attribute) && id) {
|
||||
const tempOrderTableName = `orderTable_${now}_${randomHex}`;
|
||||
try {
|
||||
await db.connection
|
||||
.raw(
|
||||
`
|
||||
CREATE TABLE :tempOrderTableName:
|
||||
SELECT
|
||||
id,
|
||||
(
|
||||
SELECT count(*)
|
||||
FROM :joinTableName: b
|
||||
WHERE a.:orderColumnName: >= b.:orderColumnName: AND a.:joinColumnName: = b.:joinColumnName: AND a.:joinColumnName: = :id
|
||||
) AS src_order
|
||||
FROM :joinTableName: a
|
||||
WHERE a.:joinColumnName: = :id
|
||||
`,
|
||||
{
|
||||
tempOrderTableName,
|
||||
joinTableName: joinTable.name,
|
||||
orderColumnName,
|
||||
joinColumnName: joinColumn.name,
|
||||
id,
|
||||
}
|
||||
)
|
||||
.transacting(trx);
|
||||
|
||||
// raw query as knex doesn't allow updating from a subquery
|
||||
// https://github.com/knex/knex/issues/2504
|
||||
await db.connection
|
||||
.raw(
|
||||
`UPDATE ?? as a, (SELECT * FROM ??) AS b
|
||||
SET ?? = b.src_order
|
||||
WHERE a.id = b.id`,
|
||||
[joinTable.name, tempOrderTableName, orderColumnName]
|
||||
)
|
||||
.transacting(trx);
|
||||
} finally {
|
||||
await db.connection.raw(`DROP TABLE IF EXISTS ??`, [tempOrderTableName]).transacting(trx);
|
||||
}
|
||||
// raw query as knex doesn't allow updating from a subquery
|
||||
// https://github.com/knex/knex/issues/2504
|
||||
const orderVar = `order_${randomSuffix}`;
|
||||
await db.connection.raw(`SET @${orderVar} = 0;`).transacting(trx);
|
||||
await db.connection
|
||||
.raw(
|
||||
`UPDATE :joinTableName: as a, (
|
||||
SELECT id, (@${orderVar}:=@${orderVar} + 1) AS src_order
|
||||
FROM :joinTableName:
|
||||
WHERE :joinColumnName: = :id
|
||||
ORDER BY :orderColumnName:
|
||||
) AS b
|
||||
SET :orderColumnName: = b.src_order
|
||||
WHERE a.id = b.id
|
||||
AND a.:joinColumnName: = :id`,
|
||||
{
|
||||
joinTableName: joinTable.name,
|
||||
orderColumnName,
|
||||
joinColumnName: joinColumn.name,
|
||||
id,
|
||||
}
|
||||
)
|
||||
.transacting(trx);
|
||||
}
|
||||
|
||||
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
||||
const tempInvOrderTableName = `invOrderTable_${now}_${randomHex}`;
|
||||
try {
|
||||
await db.connection
|
||||
.raw(
|
||||
`
|
||||
CREATE TABLE ??
|
||||
SELECT
|
||||
id,
|
||||
(
|
||||
SELECT count(*)
|
||||
FROM ?? b
|
||||
WHERE a.?? >= b.?? AND a.?? = b.?? AND a.?? IN (${inverseRelIds
|
||||
.map(() => '?')
|
||||
.join(', ')})
|
||||
) AS inv_order
|
||||
FROM ?? a
|
||||
WHERE a.?? IN (${inverseRelIds.map(() => '?').join(', ')})
|
||||
`,
|
||||
[
|
||||
tempInvOrderTableName,
|
||||
joinTable.name,
|
||||
inverseOrderColumnName,
|
||||
inverseOrderColumnName,
|
||||
inverseJoinColumn.name,
|
||||
inverseJoinColumn.name,
|
||||
inverseJoinColumn.name,
|
||||
...inverseRelIds,
|
||||
joinTable.name,
|
||||
inverseJoinColumn.name,
|
||||
...inverseRelIds,
|
||||
]
|
||||
)
|
||||
.transacting(trx);
|
||||
await db.connection
|
||||
.raw(
|
||||
`UPDATE ?? as a, (SELECT * FROM ??) AS b
|
||||
SET ?? = b.inv_order
|
||||
WHERE a.id = b.id`,
|
||||
[joinTable.name, tempInvOrderTableName, inverseOrderColumnName]
|
||||
)
|
||||
.transacting(trx);
|
||||
} finally {
|
||||
await db.connection.raw(`DROP TABLE IF EXISTS ??`, [tempInvOrderTableName]).transacting(trx);
|
||||
}
|
||||
const orderVar = `order_${randomSuffix}`;
|
||||
const columnVar = `col_${randomSuffix}`;
|
||||
await db.connection.raw(`SET @${orderVar} = 0;`).transacting(trx);
|
||||
await db.connection
|
||||
.raw(
|
||||
`UPDATE ?? as a, (
|
||||
SELECT
|
||||
id,
|
||||
@${orderVar}:=CASE WHEN @${columnVar} = ?? THEN @${orderVar} + 1 ELSE 1 END AS inv_order,
|
||||
@${columnVar}:=?? ??
|
||||
FROM ?? a
|
||||
WHERE ?? IN(${inverseRelIds.map(() => '?').join(', ')})
|
||||
ORDER BY ??, ??
|
||||
) AS b
|
||||
SET ?? = b.inv_order
|
||||
WHERE a.id = b.id
|
||||
AND a.?? IN(${inverseRelIds.map(() => '?').join(', ')})`,
|
||||
[
|
||||
joinTable.name,
|
||||
inverseJoinColumn.name,
|
||||
inverseJoinColumn.name,
|
||||
inverseJoinColumn.name,
|
||||
joinTable.name,
|
||||
inverseJoinColumn.name,
|
||||
...inverseRelIds,
|
||||
inverseJoinColumn.name,
|
||||
joinColumn.name,
|
||||
inverseOrderColumnName,
|
||||
inverseJoinColumn.name,
|
||||
...inverseRelIds,
|
||||
]
|
||||
)
|
||||
.transacting(trx);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -43,9 +43,10 @@ const createComponents = async (uid, data) => {
|
||||
throw new Error('Expected an array to create repeatable component');
|
||||
}
|
||||
|
||||
const components = await Promise.all(
|
||||
componentValue.map((value) => createComponent(componentUID, value))
|
||||
);
|
||||
const components = [];
|
||||
for (const value of componentValue) {
|
||||
components.push(await createComponent(componentUID, value));
|
||||
}
|
||||
|
||||
componentBody[attributeName] = components.map(({ id }) => {
|
||||
return {
|
||||
@ -77,18 +78,19 @@ const createComponents = async (uid, data) => {
|
||||
throw new Error('Expected an array to create repeatable component');
|
||||
}
|
||||
|
||||
componentBody[attributeName] = await Promise.all(
|
||||
dynamiczoneValues.map(async (value) => {
|
||||
const { id } = await createComponent(value.__component, value);
|
||||
return {
|
||||
id,
|
||||
__component: value.__component,
|
||||
__pivot: {
|
||||
field: attributeName,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
const dynamicZoneData = [];
|
||||
for (const value of dynamiczoneValues) {
|
||||
const { id } = await createComponent(value.__component, value);
|
||||
dynamicZoneData.push({
|
||||
id,
|
||||
__component: value.__component,
|
||||
__pivot: {
|
||||
field: attributeName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
componentBody[attributeName] = dynamicZoneData;
|
||||
|
||||
continue;
|
||||
}
|
||||
@ -137,9 +139,10 @@ const updateComponents = async (uid, entityToUpdate, data) => {
|
||||
throw new Error('Expected an array to create repeatable component');
|
||||
}
|
||||
|
||||
const components = await Promise.all(
|
||||
componentValue.map((value) => updateOrCreateComponent(componentUID, value))
|
||||
);
|
||||
const components = [];
|
||||
for (const value of componentValue) {
|
||||
components.push(await updateOrCreateComponent(componentUID, value));
|
||||
}
|
||||
|
||||
componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }) => {
|
||||
return {
|
||||
@ -173,19 +176,19 @@ const updateComponents = async (uid, entityToUpdate, data) => {
|
||||
throw new Error('Expected an array to create repeatable component');
|
||||
}
|
||||
|
||||
componentBody[attributeName] = await Promise.all(
|
||||
dynamiczoneValues.map(async (value) => {
|
||||
const { id } = await updateOrCreateComponent(value.__component, value);
|
||||
const dynamicZoneData = [];
|
||||
for (const value of dynamiczoneValues) {
|
||||
const { id } = await updateOrCreateComponent(value.__component, value);
|
||||
dynamicZoneData.push({
|
||||
id,
|
||||
__component: value.__component,
|
||||
__pivot: {
|
||||
field: attributeName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
__component: value.__component,
|
||||
__pivot: {
|
||||
field: attributeName,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
componentBody[attributeName] = dynamicZoneData;
|
||||
|
||||
continue;
|
||||
}
|
||||
@ -287,14 +290,14 @@ const deleteComponents = async (uid, entityToDelete, { loadComponents = true } =
|
||||
|
||||
if (attribute.type === 'component') {
|
||||
const { component: componentUID } = attribute;
|
||||
await Promise.all(
|
||||
_.castArray(value).map((subValue) => deleteComponent(componentUID, subValue))
|
||||
);
|
||||
for (const subValue of _.castArray(value)) {
|
||||
await deleteComponent(componentUID, subValue);
|
||||
}
|
||||
} else {
|
||||
// delete dynamic zone components
|
||||
await Promise.all(
|
||||
_.castArray(value).map((subValue) => deleteComponent(subValue.__component, subValue))
|
||||
);
|
||||
for (const subValue of _.castArray(value)) {
|
||||
await deleteComponent(subValue.__component, subValue);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
|
||||
@ -228,6 +228,7 @@ module.exports = ({ strapi }) => ({
|
||||
const formats = await generateResponsiveFormats(fileData);
|
||||
if (Array.isArray(formats) && formats.length > 0) {
|
||||
for (const format of formats) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!format) continue;
|
||||
uploadPromises.push(uploadResponsiveFormat(format));
|
||||
}
|
||||
|
||||
@ -32,7 +32,9 @@ const syncLocalizations = async (entry, { model }) => {
|
||||
return strapi.query(model.uid).update({ where: { id }, data: { localizations } });
|
||||
};
|
||||
|
||||
await Promise.all(entry.localizations.map(({ id }) => updateLocalization(id)));
|
||||
for (const localization of entry.localizations) {
|
||||
await updateLocalization(localization.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -56,7 +58,9 @@ const syncNonLocalizedAttributes = async (entry, { model }) => {
|
||||
return strapi.entityService.update(model.uid, id, { data: nonLocalizedAttributes });
|
||||
};
|
||||
|
||||
await Promise.all(entry.localizations.map(({ id }) => updateLocalization(id)));
|
||||
for (const localization of entry.localizations) {
|
||||
await updateLocalization(localization.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -173,6 +173,21 @@ const forms = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
intlLabel: {
|
||||
id: getTrad({ id: 'PopUpForm.Providers.jwksurl.label' }),
|
||||
defaultMessage: 'JWKS URL',
|
||||
},
|
||||
name: 'jwksurl',
|
||||
type: 'text',
|
||||
placeholder: textPlaceholder,
|
||||
size: 12,
|
||||
validations: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
{
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
"@strapi/utils": "4.5.6",
|
||||
"bcryptjs": "2.4.3",
|
||||
"grant-koa": "5.4.8",
|
||||
"jwk-to-pem": "2.0.5",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
"koa": "^2.13.4",
|
||||
"koa2-ratelimit": "^1.1.2",
|
||||
@ -64,4 +65,4 @@
|
||||
"required": true,
|
||||
"kind": "plugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,48 @@
|
||||
|
||||
const { strict: assert } = require('assert');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const jwkToPem = require('jwk-to-pem');
|
||||
|
||||
const getCognitoPayload = async ({ idToken, jwksUrl, purest }) => {
|
||||
const {
|
||||
header: { kid },
|
||||
payload,
|
||||
} = jwt.decode(idToken, { complete: true });
|
||||
|
||||
if (!payload || !kid) {
|
||||
throw new Error('The provided token is not valid');
|
||||
}
|
||||
|
||||
const config = {
|
||||
cognito: {
|
||||
discovery: {
|
||||
origin: jwksUrl.origin,
|
||||
path: jwksUrl.pathname,
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const cognito = purest({ provider: 'cognito', config });
|
||||
// get the JSON Web Key (JWK) for the user pool
|
||||
const { body: jwk } = await cognito('discovery').request();
|
||||
// Get the key with the same Key ID as the provided token
|
||||
const key = jwk.keys.find(({ kid: jwkKid }) => jwkKid === kid);
|
||||
const pem = jwkToPem(key);
|
||||
|
||||
// https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
|
||||
const decodedToken = await new Promise((resolve, reject) => {
|
||||
jwt.verify(idToken, pem, { algorithms: ['RS256'] }, (err, decodedToken) => {
|
||||
if (err) {
|
||||
reject();
|
||||
}
|
||||
resolve(decodedToken);
|
||||
});
|
||||
});
|
||||
return decodedToken;
|
||||
} catch (err) {
|
||||
throw new Error('There was an error verifying the token');
|
||||
}
|
||||
};
|
||||
|
||||
const getInitialProviders = ({ purest }) => ({
|
||||
async discord({ accessToken }) {
|
||||
@ -19,19 +61,14 @@ const getInitialProviders = ({ purest }) => ({
|
||||
};
|
||||
});
|
||||
},
|
||||
async cognito({ query }) {
|
||||
// get the id_token
|
||||
async cognito({ query, providers }) {
|
||||
const jwksUrl = new URL(providers.cognito.jwksurl);
|
||||
const idToken = query.id_token;
|
||||
// decode the jwt token
|
||||
const tokenPayload = jwt.decode(idToken);
|
||||
if (!tokenPayload) {
|
||||
throw new Error('unable to decode jwt token');
|
||||
} else {
|
||||
return {
|
||||
username: tokenPayload['cognito:username'],
|
||||
email: tokenPayload.email,
|
||||
};
|
||||
}
|
||||
const tokenPayload = await getCognitoPayload({ idToken, jwksUrl, purest });
|
||||
return {
|
||||
username: tokenPayload['cognito:username'],
|
||||
email: tokenPayload.email,
|
||||
};
|
||||
},
|
||||
async facebook({ accessToken }) {
|
||||
const facebook = purest({ provider: 'facebook' });
|
||||
|
||||
@ -14847,7 +14847,7 @@ jwa@^2.0.0:
|
||||
ecdsa-sig-formatter "1.0.11"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jwk-to-pem@^2.0.5:
|
||||
jwk-to-pem@2.0.5, jwk-to-pem@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz#151310bcfbcf731adc5ad9f379cbc8b395742906"
|
||||
integrity sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user