From 98e42ec6ffd245f522bc1cbceda0d3f84d3330b5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 14 May 2022 15:00:15 +0900 Subject: enhance: Display TOTP Register URL Close #7261 Co-Authored-By: tamaina --- packages/backend/src/server/api/endpoints/i/2fa/register.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index d5e1b19e54..33f5717728 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -2,8 +2,8 @@ import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import * as QRCode from 'qrcode'; import config from '@/config/index.js'; -import define from '../../../define.js'; import { UserProfiles } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { requireCredential: true, @@ -40,15 +40,17 @@ export default define(meta, paramDef, async (ps, user) => { }); // Get the data URL of the authenticator URL - const dataUrl = await QRCode.toDataURL(speakeasy.otpauthURL({ + const url = speakeasy.otpauthURL({ secret: secret.base32, encoding: 'base32', label: user.username, issuer: config.host, - })); + }); + const dataUrl = await QRCode.toDataURL(url); return { qr: dataUrl, + url, secret: secret.base32, label: user.username, issuer: config.host, -- cgit v1.2.3-freya From 4b872856c2c79ea4e604af481cd2c78487993d88 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sat, 14 May 2022 08:09:10 +0200 Subject: fix: keep file order (#8659) --- packages/backend/src/server/api/endpoints/notes/create.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 40a3ba73ca..ff62841a0c 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -172,10 +172,14 @@ export default define(meta, paramDef, async (ps, user) => { let files: DriveFile[] = []; const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; if (fileIds != null) { - files = await DriveFiles.findBy({ - userId: user.id, - id: In(fileIds), - }); + files = await DriveFiles.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: user.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id")') + .setParameters({ fileIds }) + .getMany(); } let renote: Note | null = null; -- cgit v1.2.3-freya From b2a5076d14e84fc427e9ab59ae373b04ddfbf40c Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 14 May 2022 15:24:44 +0900 Subject: fix: ユーザー検索で、クエリがusernameの条件を満たす場合はusernameもLIKE検索するように (#8644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix #8643 * 部分一致にする --- packages/backend/src/server/api/endpoints/users/search.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index a72a58a843..f93d4f718b 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -61,7 +61,14 @@ export default define(meta, paramDef, async (ps, me) => { .getMany(); } else { const nameQuery = Users.createQueryBuilder('user') - .where('user.name ILIKE :query', { query: '%' + ps.query + '%' }) + .where(new Brackets(qb => { + qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' }); + + // Also search username if it qualifies as username + if (Users.validateLocalUsername(ps.query)) { + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' }); + } + })) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); -- cgit v1.2.3-freya From b21b0580058c14532ff3f4033e2a9147643bfca6 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 15 May 2022 12:18:46 +0900 Subject: feat: make captcha required when signin to improve security --- packages/backend/src/server/api/private/signin.ts | 25 +++++++++++++++++++---- packages/client/src/components/signin.vue | 17 ++++++++++++--- packages/client/src/components/signup.vue | 6 +++--- 3 files changed, 38 insertions(+), 10 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index 7b66657ad8..e8b222a4d5 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -1,20 +1,37 @@ +import { randomBytes } from 'node:crypto'; import Koa from 'koa'; import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; -import signin from '../common/signin.js'; +import { IsNull } from 'typeorm'; import config from '@/config/index.js'; import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js'; import { ILocalUser } from '@/models/entities/user.js'; import { genId } from '@/misc/gen-id.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; +import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js'; import { verifyLogin, hash } from '../2fa.js'; -import { randomBytes } from 'node:crypto'; -import { IsNull } from 'typeorm'; +import signin from '../common/signin.js'; export default async (ctx: Koa.Context) => { ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Credentials', 'true'); const body = ctx.request.body as any; + + const instance = await fetchMeta(true); + + if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { + await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { + await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + const username = body['username']; const password = body['password']; const token = body['token']; @@ -155,7 +172,7 @@ export default async (ctx: Koa.Context) => { body.credentialId .replace(/-/g, '+') .replace(/_/g, '/'), - 'base64' + 'base64', ).toString('hex'), }); diff --git a/packages/client/src/components/signin.vue b/packages/client/src/components/signin.vue index bdf247a56f..4f88e1829c 100644 --- a/packages/client/src/components/signin.vue +++ b/packages/client/src/components/signin.vue @@ -33,6 +33,8 @@ + + {{ signing ? $ts.loggingIn : $ts.login }} @@ -60,6 +62,7 @@ export default defineComponent({ components: { MkButton, MkInput, + MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')), }, props: { @@ -90,6 +93,8 @@ export default defineComponent({ credential: null, challengeData: null, queryingKey: false, + hCaptchaResponse: null, + reCaptchaResponse: null, }; }, @@ -139,11 +144,13 @@ export default defineComponent({ return os.api('signin', { username: this.username, password: this.password, + 'hcaptcha-response': this.hCaptchaResponse, + 'g-recaptcha-response': this.reCaptchaResponse, signature: hexify(credential.response.signature), authenticatorData: hexify(credential.response.authenticatorData), clientDataJSON: hexify(credential.response.clientDataJSON), credentialId: credential.id, - challengeId: this.challengeData.challengeId + challengeId: this.challengeData.challengeId, }); }).then(res => { this.$emit('login', res); @@ -164,7 +171,9 @@ export default defineComponent({ if (window.PublicKeyCredential && this.user.securityKeys) { os.api('signin', { username: this.username, - password: this.password + password: this.password, + 'hcaptcha-response': this.hCaptchaResponse, + 'g-recaptcha-response': this.reCaptchaResponse, }).then(res => { this.totpLogin = true; this.signing = false; @@ -179,7 +188,9 @@ export default defineComponent({ os.api('signin', { username: this.username, password: this.password, - token: this.user && this.user.twoFactorEnabled ? this.token : undefined + 'hcaptcha-response': this.hCaptchaResponse, + 'g-recaptcha-response': this.reCaptchaResponse, + token: this.user && this.user.twoFactorEnabled ? this.token : undefined, }).then(res => { this.$emit('login', res); this.onLogin(res); diff --git a/packages/client/src/components/signup.vue b/packages/client/src/components/signup.vue index 62f370ffa8..aeed0e53fa 100644 --- a/packages/client/src/components/signup.vue +++ b/packages/client/src/components/signup.vue @@ -58,8 +58,8 @@ - - + + {{ $ts.start }} @@ -81,7 +81,7 @@ export default defineComponent({ MkButton, MkInput, MkSwitch, - captcha: defineAsyncComponent(() => import('./captcha.vue')), + MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')), }, props: { -- cgit v1.2.3-freya From 02a43a310f6ad0cc9e9beccc26e51ab5b339e15f Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 15 May 2022 16:47:14 +0900 Subject: CAPTCHA求めるのは2fa認証が無効になっているときだけにした MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2faのトークンは期限付きだから、CAPTCHA解いてる間に期限切れになる --- packages/backend/src/server/api/private/signin.ts | 24 +++++++++++------------ packages/client/src/components/signin.vue | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index e8b222a4d5..0024b8ce3e 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -20,18 +20,6 @@ export default async (ctx: Koa.Context) => { const instance = await fetchMeta(true); - if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { - await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => { - ctx.throw(400, e); - }); - } - - if (instance.enableRecaptcha && instance.recaptchaSecretKey) { - await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => { - ctx.throw(400, e); - }); - } - const username = body['username']; const password = body['password']; const token = body['token']; @@ -96,6 +84,18 @@ export default async (ctx: Koa.Context) => { } if (!profile.twoFactorEnabled) { + if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { + await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { + await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + if (same) { signin(ctx, user); return; diff --git a/packages/client/src/components/signin.vue b/packages/client/src/components/signin.vue index 4f88e1829c..d140e143d3 100644 --- a/packages/client/src/components/signin.vue +++ b/packages/client/src/components/signin.vue @@ -11,6 +11,8 @@ + + {{ signing ? $ts.loggingIn : $ts.login }} -- cgit v1.2.3-freya From 037ca92275bd8917eb7bf6f62f7613adc2bbaf36 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 15 May 2022 11:32:00 +0200 Subject: fix: postgres type error Fix a bug introduced in #8659. Solution was already tested there. --- packages/backend/src/server/api/endpoints/notes/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index ff62841a0c..955f53bbc1 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -177,7 +177,7 @@ export default define(meta, paramDef, async (ps, user) => { userId: user.id, fileIds, }) - .orderBy('array_position(ARRAY[:...fileIds], "id")') + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') .setParameters({ fileIds }) .getMany(); } -- cgit v1.2.3-freya From aaf5bb62abd6c1daefc675a7aa7eebfac561fb3a Mon Sep 17 00:00:00 2001 From: Johann150 Date: Thu, 19 May 2022 09:54:45 +0200 Subject: enhance: uniform theme color (#8702) * enhance: make theme color format uniform All newly fetched instance theme colors will be uniformely formatted as hashtag followed by 6 hexadecimal digits. Colors are checked for validity and invalid colors are not handled. * better input validation for own theme color * migration to unify theme color formats Fixes theme colors of other instances as well as the local instance. * add changelog entry Co-authored-by: syuilo --- CHANGELOG.md | 3 ++ .../migration/1652859567549-uniform-themecolor.js | 38 ++++++++++++++++++++++ .../src/server/api/endpoints/admin/update-meta.ts | 2 +- .../src/services/fetch-instance-metadata.ts | 14 +++----- 4 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 packages/backend/migration/1652859567549-uniform-themecolor.js (limited to 'packages/backend/src/server/api') diff --git a/CHANGELOG.md b/CHANGELOG.md index fb8b8fdee6..21ae948d0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ You should also include the user name that made the change. - update dependencies @syuilo - enhance: display URL of QR code for TOTP registration @syuilo - make CAPTCHA required for signin to improve security @syuilo +- The theme color is now better validated. @Johann150 + Your own theme color may be unset if it was in an invalid format. + Admins should check their instance settings if in doubt. - Perform port diagnosis at startup only when Listen fails @mei23 ### Bugfixes diff --git a/packages/backend/migration/1652859567549-uniform-themecolor.js b/packages/backend/migration/1652859567549-uniform-themecolor.js new file mode 100644 index 0000000000..bc47143e54 --- /dev/null +++ b/packages/backend/migration/1652859567549-uniform-themecolor.js @@ -0,0 +1,38 @@ +import tinycolor from 'tinycolor2'; + +export class uniformThemecolor1652859567549 { + name = 'uniformThemecolor1652859567549' + + async up(queryRunner) { + const formatColor = (color) => { + let tc = new tinycolor(color); + if (color.isValid()) { + return color.toHexString(); + } else { + return null; + } + }; + + await Promise.all(queryRunner.query('SELECT "id", "themeColor" FROM "instance" WHERE "themeColor" IS NOT NULL') + .then(instances => instances.map(instance => { + // update theme color to uniform format, e.g. #00ff00 + // invalid theme colors get set to null + instance.color = formatColor(instance.color); + + return queryRunner.query('UPDATE "instance" SET "themeColor" = :themeColor WHERE "id" = :id', instance); + }))); + + // also fix own theme color + await queryRunner.query('SELECT "themeColor" FROM "meta" WHERE "themeColor" IS NOT NULL LIMIT 1') + .then(metas => { + if (metas.length > 0) { + return queryRunner.query('UPDATE "meta" SET "themeColor" = :color', { color: formatColor(metas[0].color) }); + } + }); + } + + async down(queryRunner) { + // The original representation is not stored, so migrating back is not possible. + // The new format also works in older versions so this is not a problem. + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index b23ee9e3df..09e43301b7 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -27,7 +27,7 @@ export const paramDef = { blockedHosts: { type: 'array', nullable: true, items: { type: 'string', } }, - themeColor: { type: 'string', nullable: true }, + themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, errorImageUrl: { type: 'string', nullable: true }, diff --git a/packages/backend/src/services/fetch-instance-metadata.ts b/packages/backend/src/services/fetch-instance-metadata.ts index d5294c5fe8..029c388dc2 100644 --- a/packages/backend/src/services/fetch-instance-metadata.ts +++ b/packages/backend/src/services/fetch-instance-metadata.ts @@ -1,5 +1,6 @@ import { DOMWindow, JSDOM } from 'jsdom'; import fetch from 'node-fetch'; +import tinycolor from 'tinycolor2'; import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js'; import { Instance } from '@/models/entities/instance.js'; import { Instances } from '@/models/index.js'; @@ -208,16 +209,11 @@ async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | nul } async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record | null): Promise { - if (doc) { - const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content'); + const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color; - if (themeColor) { - return themeColor; - } - } - - if (manifest) { - return manifest.theme_color; + if (themeColor) { + const color = new tinycolor(themeColor); + if (color.isValid()) return color.toHexString(); } return null; -- cgit v1.2.3-freya From 63a814c70e96a5d39738e4157b4e94fb29c93f2e Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 27 May 2022 15:03:25 +0200 Subject: fix(docs): correct information for drive upload (#8736) --- packages/backend/src/server/api/openapi/gen-spec.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index c6e557aefb..3929fff3f7 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -59,6 +59,18 @@ export function genOpenapiSpec(lang = 'ja-JP') { desc += ` / **Permission**: *${kind}*`; } + const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; + const schema = endpoint.params; + + if (endpoint.meta.requireFile) { + schema.properties.file = { + type: 'string', + format: 'binary', + description: 'The file contents.', + }; + schema.required.push('file'); + } + const info = { operationId: endpoint.name, summary: endpoint.name, @@ -78,8 +90,8 @@ export function genOpenapiSpec(lang = 'ja-JP') { requestBody: { required: true, content: { - 'application/json': { - schema: endpoint.params, + [requestType]: { + schema, }, }, }, -- cgit v1.2.3-freya