diff --git a/.eslintrc.json b/.eslintrc.json
index f23b233..40eaa8f 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -60,7 +60,6 @@
"no-lone-blocks": "error",
"no-loop-func": "error",
"no-multi-spaces": "error",
- "no-new": "error",
"no-new-wrappers": "error",
"no-octal": "error",
"no-octal-escape": "error",
diff --git a/packages/website/astro.config.ts b/packages/website/astro.config.ts
index 922c61f..a478a90 100644
--- a/packages/website/astro.config.ts
+++ b/packages/website/astro.config.ts
@@ -26,11 +26,16 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
+// Astro only uses process.env for variables visible during build time
+// As I do not want anything from .env to be inlined in server bundle, I load everything with dotenv
+import 'dotenv/config'
+
import { defineConfig } from 'astro/config'
import tailwind from '@astrojs/tailwind'
import node from '@astrojs/node'
export default defineConfig({
+ site: 'https://pronoundb.org/',
output: 'server',
integrations: [ tailwind() ],
adapter: node({ mode: 'standalone' }),
diff --git a/packages/website/package.json b/packages/website/package.json
index 4f75e77..27d354e 100644
--- a/packages/website/package.json
+++ b/packages/website/package.json
@@ -25,6 +25,7 @@
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.49.0",
"astro": "^2.0.2",
+ "dotenv": "^16.0.3",
"eslint": "^8.32.0",
"feather-icons": "^4.29.0",
"simple-icons": "^8.3.0",
diff --git a/packages/website/src/layouts/Layout.astro b/packages/website/src/layouts/Layout.astro
index 049ba7e..bd79269 100644
--- a/packages/website/src/layouts/Layout.astro
+++ b/packages/website/src/layouts/Layout.astro
@@ -41,7 +41,11 @@ export interface Props {
const DEFAULT_DESCRIPTION = 'PronounDB is a browser extension that helps people know each other\'s pronouns easily and instantly.'
-const { title, description, flash } = Astro.props
+const { title: t, description: d, flash } = Astro.props
+const description = d || DEFAULT_DESCRIPTION
+const title = t ? `${t} • PronounDB` : 'PronounDB'
+const canonicalUrl = new URL(Astro.url.pathname, Astro.site)
+
const [ flashIcon, flashColor ] = flash?.startsWith('S_')
? [ checkSvg, 'text-green-700 border-green-700 dark:text-green-400 dark:border-green-400' ]
: [ alertSvg, 'text-red-600 border-red-600 dark:text-red-orange dark:border-red-orange' ]
@@ -50,14 +54,23 @@ const [ flashIcon, flashColor ] = flash?.startsWith('S_')
-
+
+
+ {title}
+
+
+
+
+
+
+
+
-
-
- {title ? `${title} • PronounDB` : 'PronounDB'}
-
-
+
+
+
+
diff --git a/packages/website/src/pages/api/v1/lookup-bulk.ts b/packages/website/src/pages/api/v1/lookup-bulk.ts
index 3f6f5f5..3125c05 100644
--- a/packages/website/src/pages/api/v1/lookup-bulk.ts
+++ b/packages/website/src/pages/api/v1/lookup-bulk.ts
@@ -62,8 +62,8 @@ export async function get (ctx: APIContext) {
const res = Object.create(null)
let user: PronounsOfUser | null
while ((user = await cursor.next())) {
- res[user.id] = user.pronouns
- ids.delete(user.id)
+ res[user.account.id] = user.pronouns
+ ids.delete(user.account.id)
}
await cursor.close()
diff --git a/packages/website/src/pages/me/delete.ts b/packages/website/src/pages/me/delete.ts
index 70aee1a..a5a2d79 100644
--- a/packages/website/src/pages/me/delete.ts
+++ b/packages/website/src/pages/me/delete.ts
@@ -27,9 +27,10 @@
*/
import type { APIContext } from 'astro'
+import { DeletedAccountCount } from '@server/metrics.js'
import { authenticate, validateCsrf } from '@server/auth.js'
-import { setFlash } from '@server/flash.js'
import { deleteAccount } from '@server/database/account.js'
+import { setFlash } from '@server/flash.js'
export async function post (ctx: APIContext) {
const user = await authenticate(ctx)
@@ -43,6 +44,7 @@ export async function post (ctx: APIContext) {
return ctx.redirect('/me')
}
+ DeletedAccountCount.inc()
deleteAccount(user._id)
ctx.cookies.delete('token')
setFlash(ctx, 'S_ACC_DELETED')
diff --git a/packages/website/src/pages/me/unlink-account.ts b/packages/website/src/pages/me/unlink-account.ts
index f7998bb..98bef32 100644
--- a/packages/website/src/pages/me/unlink-account.ts
+++ b/packages/website/src/pages/me/unlink-account.ts
@@ -27,9 +27,10 @@
*/
import type { APIContext } from 'astro'
+import { LinkedAccountsRemovalCount } from '@server/metrics.js'
import { authenticate, validateCsrf } from '@server/auth.js'
-import { setFlash } from '@server/flash.js'
import { removeLinkedAccount } from '@server/database/account.js'
+import { setFlash } from '@server/flash.js'
export async function post (ctx: APIContext) {
const user = await authenticate(ctx)
@@ -50,6 +51,7 @@ export async function post (ctx: APIContext) {
return new Response('400: Bad request', { status: 400 })
}
+ LinkedAccountsRemovalCount.inc({ platform: platform })
removeLinkedAccount(user._id, platform, id)
return ctx.redirect('/me')
}
diff --git a/packages/website/src/pages/oauth/[platform]/callback.ts b/packages/website/src/pages/oauth/[platform]/callback.ts
index 5da2c14..07e68e5 100644
--- a/packages/website/src/pages/oauth/[platform]/callback.ts
+++ b/packages/website/src/pages/oauth/[platform]/callback.ts
@@ -28,6 +28,7 @@
import type { APIContext } from 'astro'
+import { CreatedAccountCount, LinkedAccountsAddCount } from '@server/metrics.js'
import { generateToken, authenticate } from '@server/auth.js'
import { type FlashMessage, setFlash } from '@server/flash.js'
import { type ExternalAccount, createAccount, findByExternalAccount, addLinkedAccount } from '@server/database/account.js'
@@ -81,6 +82,7 @@ export async function get (ctx: APIContext) {
}
if (!existingAccount) {
+ LinkedAccountsAddCount.inc({ platform: external.platform })
await addLinkedAccount(user!._id, external)
}
@@ -96,6 +98,11 @@ export async function get (ctx: APIContext) {
return ctx.redirect('/')
}
+ if (intent === 'register') {
+ CreatedAccountCount.inc({ platform: external.platform })
+ LinkedAccountsAddCount.inc({ platform: external.platform })
+ }
+
const authToken = generateToken({ id: account.toString() })
ctx.cookies.set('token', authToken, { path: '/', maxAge: 365 * 24 * 3600, httpOnly: true, secure: import.meta.env.PROD })
if (intent === 'register') setFlash(ctx, 'S_REGISTERED')
diff --git a/packages/website/src/pages/shields/[id].json.ts b/packages/website/src/pages/shields/[id].json.ts
index 6bb78c5..08eb0a5 100644
--- a/packages/website/src/pages/shields/[id].json.ts
+++ b/packages/website/src/pages/shields/[id].json.ts
@@ -55,7 +55,6 @@ export const LegacyPronouns: Record = {
avoid: 'Avoid pronouns, use my name',
}
-
function formatPronouns (pronounsId: string, capitalize: boolean) {
const pronouns = LegacyPronouns[pronounsId]
return Array.isArray(pronouns) ? pronouns[capitalize ? 1 : 0] : pronouns
diff --git a/packages/website/src/server/auth.ts b/packages/website/src/server/auth.ts
index 95cf16f..3df5dba 100644
--- a/packages/website/src/server/auth.ts
+++ b/packages/website/src/server/auth.ts
@@ -30,6 +30,7 @@ import type { APIContext } from 'astro'
import { ObjectId } from 'mongodb'
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'crypto'
import { createSigner, createVerifier } from 'fast-jwt'
+import { LegacyTokenizeMigrationCounter } from './metrics.js'
import { findById } from './database/account.js'
export type JwtPayload = { id: string }
@@ -81,10 +82,9 @@ export async function authenticate ({ cookies }: APIContext, lax?: boolean) {
try {
const { id } = verifier(token)
- const user = await findById(new ObjectId(id))
- if (!user) cookies.delete('token')
- return user
+ return findById(new ObjectId(id))
} catch {
+ // Not deleting the token if invalid is intentional. This allows the extension to still show up pronouns.
return null
}
}
@@ -117,11 +117,11 @@ export function validateCsrf ({ cookies }: APIContext, csrf: string) {
// Legacy tokenize migration
export function migrateAuth ({ cookies }: APIContext) {
+ if (!import.meta.env.LEGACY_SECRET_KEY) return
+
let token = cookies.get('token').value
if (!token || token.startsWith('ey')) return
- if (!import.meta.env.LEGACY_SECRET_KEY) return
-
const [ id, gen, sig ] = token.split('.')
if (!id || !gen || !sig) return
@@ -132,6 +132,7 @@ export function migrateAuth ({ cookies }: APIContext) {
if (!safeEqual(sig, expectedSig)) return
+ LegacyTokenizeMigrationCounter.inc()
token = generateToken({ id: Buffer.from(id, 'base64').toString() })
cookies.set('token', token, { path: '/', maxAge: 365 * 24 * 3600, httpOnly: true, secure: import.meta.env.PROD })
}
diff --git a/packages/website/src/server/database/account.ts b/packages/website/src/server/database/account.ts
index a3639eb..ee8c100 100644
--- a/packages/website/src/server/database/account.ts
+++ b/packages/website/src/server/database/account.ts
@@ -29,7 +29,9 @@
import type { ObjectId } from 'mongodb'
import database from './database.js'
-const collection = database.collection('accounts')
+export const collection = database.collection('accounts')
+await collection.createIndex({ 'accounts.id': 1, 'accounts.platform': 1 })
+await collection.createIndex({ 'accounts.platform': 1 })
export type Account = {
pronouns: string
@@ -44,8 +46,7 @@ export type ExternalAccount = {
export type PronounsOfUser = {
pronouns: string
- platform: string
- id: string
+ account: ExternalAccount
}
export async function createAccount (from: ExternalAccount) {
@@ -75,20 +76,34 @@ export async function findByExternalAccount (external: ExternalAccount) {
}
export function findPronounsOf (platform: string, externalIds: string[]) {
+ // perf: first filtering ($match) needs to be done before doing anything to ensure we hit the index.
+ // once initial filtering is done, we can do whatever as the dataset is small enough.
+ // it was behaving with 10k+ docs, it should be ridiculously fast for <=50 docs...
return collection.aggregate([
- { $unwind: '$accounts' },
{
- '$match': {
+ $match: {
'accounts.platform': platform,
- 'accounts.id': { '$in': externalIds },
+ 'accounts.id': { $in: externalIds },
},
},
{
$project: {
_id: 0,
pronouns: 1,
- platform: '$accounts.platform',
- id: '$accounts.id',
+ account: {
+ $first: {
+ $filter: {
+ input: '$accounts',
+ as: 'account',
+ cond: {
+ $and: [
+ { $eq: [ '$$account.platform', platform ] },
+ { $in: [ '$$account.id', externalIds ] },
+ ],
+ },
+ },
+ },
+ },
},
},
])
diff --git a/packages/website/src/server/flash.ts b/packages/website/src/server/flash.ts
index af6e0b7..6d1f0bc 100644
--- a/packages/website/src/server/flash.ts
+++ b/packages/website/src/server/flash.ts
@@ -36,7 +36,7 @@ export const FlashMessages = {
S_ACC_DELETED: 'Your account has been successfully deleted. Sorry to see you go!',
// Error
- E_CSRF: 'Verification of the authenticity of the submission failed. Please try again.',
+ E_CSRF: 'Verification of the authenticity of the submission failed (CSRF check). Please try again.',
E_OAUTH_GENERIC: 'An unknown error occurred while authenticating with the third party service.',
E_OAUTH_FETCH: 'Could not fetch information about your external account.',
E_OAUTH_10A_EXCHANGE: 'Could not initialize the authentication request with the third party.',
diff --git a/packages/website/src/server/metrics.ts b/packages/website/src/server/metrics.ts
index be749e5..1f785fb 100644
--- a/packages/website/src/server/metrics.ts
+++ b/packages/website/src/server/metrics.ts
@@ -26,14 +26,68 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-import { register, collectDefaultMetrics, Counter, Histogram } from 'prom-client'
+import { register, collectDefaultMetrics, Counter, Histogram, Gauge } from 'prom-client'
+import { collection } from './database/account.js'
+
+const providersGlob = import.meta.glob('../server/oauth/platforms/*.ts', { eager: true })
+const providers = Object.keys(providersGlob).map((k) => k.slice(26, -3))
collectDefaultMetrics({
prefix: 'pronoundb_',
})
/// ACCOUNT METRICS
-// todo
+new Gauge({
+ name: 'pronoundb_accounts_total',
+ help: 'accounts count',
+ labelNames: [],
+ collect: async function () {
+ const count = await collection.countDocuments()
+ this.set({}, count)
+ },
+})
+
+new Gauge({
+ name: 'pronoundb_linked_accounts_total',
+ help: 'accounts linked per platform count',
+ labelNames: [ 'platform' ],
+ collect: async function () {
+ // - But Cynthia!!!! You're doing this every 30 seconds!!!!
+ // - lol yea whatever hehe
+ // - ................
+ // - *noms cookie*
+ await Promise.all(
+ providers.map(
+ (p) => collection.countDocuments({ 'accounts.platform': p })
+ .then((count) => this.set({ platform: p }, count))
+ )
+ )
+ },
+})
+
+export const CreatedAccountCount = new Counter({
+ name: 'pronoundb_account_create_count',
+ help: 'amount of accounts created',
+ labelNames: [ 'platform' ],
+})
+
+export const DeletedAccountCount = new Counter({
+ name: 'pronoundb_account_deletion_count',
+ help: 'amount of accounts deleted',
+ labelNames: [],
+})
+
+export const LinkedAccountsAddCount = new Counter({
+ name: 'pronoundb_linked_accounts_add_count',
+ help: 'amount of accounts linked per platform',
+ labelNames: [ 'platform' ],
+})
+
+export const LinkedAccountsRemovalCount = new Counter({
+ name: 'pronoundb_linked_accounts_remove_count',
+ help: 'amount of accounts unlinked per platform',
+ labelNames: [ 'platform' ],
+})
/// LOOKUP METRICS
export const LookupRequestsCounter = new Counter({
@@ -61,7 +115,13 @@ export const LookupHitCounter = new Counter({
})
/// INTERNAL HEALTH METRICS
-// todo
+// some more metrics might be welcome
+// for now I just log this one so I can know roughly when I can ditch Tokenize migration code
+export const LegacyTokenizeMigrationCounter = new Counter({
+ name: 'pronoundb_tokenize_migration_total',
+ help: 'tokens migrated from legacy tokenize to jwt',
+ labelNames: [],
+})
/// HELPERS
export function metrics () {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8a31b17..b93b24c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -56,6 +56,7 @@ importers:
'@typescript-eslint/eslint-plugin': ^5.49.0
'@typescript-eslint/parser': ^5.49.0
astro: ^2.0.2
+ dotenv: ^16.0.3
eslint: ^8.32.0
fast-jwt: ^2.1.0
feather-icons: ^4.29.0
@@ -75,6 +76,7 @@ importers:
'@typescript-eslint/eslint-plugin': 5.49.0_iu322prlnwsygkcra5kbpy22si
'@typescript-eslint/parser': 5.49.0_7uibuqfxkfaozanbtbziikiqje
astro: 2.0.2_@types+node@18.11.18
+ dotenv: 16.0.3
eslint: 8.32.0
feather-icons: 4.29.0
simple-icons: 8.3.0
@@ -2713,6 +2715,11 @@ packages:
domhandler: 4.3.1
dev: true
+ /dotenv/16.0.3:
+ resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
+ engines: {node: '>=12'}
+ dev: true
+
/dset/3.1.2:
resolution: {integrity: sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==}
engines: {node: '>=4'}
@@ -4416,17 +4423,6 @@ packages:
hasBin: true
dev: true
- /postcss-import/14.1.0:
- resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
- engines: {node: '>=10.0.0'}
- peerDependencies:
- postcss: ^8.0.0
- dependencies:
- postcss-value-parser: 4.2.0
- read-cache: 1.0.0
- resolve: 1.22.1
- dev: true
-
/postcss-import/14.1.0_postcss@8.4.21:
resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
engines: {node: '>=10.0.0'}
@@ -4439,15 +4435,6 @@ packages:
resolve: 1.22.1
dev: true
- /postcss-js/4.0.0:
- resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==}
- engines: {node: ^12 || ^14 || >= 16}
- peerDependencies:
- postcss: ^8.3.3
- dependencies:
- camelcase-css: 2.0.1
- dev: true
-
/postcss-js/4.0.0_postcss@8.4.21:
resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==}
engines: {node: ^12 || ^14 || >= 16}
@@ -4458,22 +4445,6 @@ packages:
postcss: 8.4.21
dev: true
- /postcss-load-config/3.1.4:
- resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
- engines: {node: '>= 10'}
- peerDependencies:
- postcss: '>=8.0.9'
- ts-node: '>=9.0.0'
- peerDependenciesMeta:
- postcss:
- optional: true
- ts-node:
- optional: true
- dependencies:
- lilconfig: 2.0.6
- yaml: 1.10.2
- dev: true
-
/postcss-load-config/3.1.4_postcss@8.4.21:
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'}
@@ -4508,15 +4479,6 @@ packages:
yaml: 2.2.1
dev: true
- /postcss-nested/6.0.0:
- resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==}
- engines: {node: '>=12.0'}
- peerDependencies:
- postcss: ^8.2.14
- dependencies:
- postcss-selector-parser: 6.0.11
- dev: true
-
/postcss-nested/6.0.0_postcss@8.4.21:
resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==}
engines: {node: '>=12.0'}
@@ -5223,8 +5185,6 @@ packages:
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
engines: {node: '>=12.13.0'}
hasBin: true
- peerDependencies:
- postcss: ^8.0.9
dependencies:
arg: 5.0.2
chokidar: 3.5.3
@@ -5241,10 +5201,10 @@ packages:
object-hash: 3.0.0
picocolors: 1.0.0
postcss: 8.4.21
- postcss-import: 14.1.0
- postcss-js: 4.0.0
- postcss-load-config: 3.1.4
- postcss-nested: 6.0.0
+ postcss-import: 14.1.0_postcss@8.4.21
+ postcss-js: 4.0.0_postcss@8.4.21
+ postcss-load-config: 3.1.4_postcss@8.4.21
+ postcss-nested: 6.0.0_postcss@8.4.21
postcss-selector-parser: 6.0.11
postcss-value-parser: 4.2.0
quick-lru: 5.1.1