Merge branch 'gravatar-sha256-p' into 'main'

Gravatar sha256

See merge request PronounsPage/PronounsPage!605
This commit is contained in:
Valentyne Stigloher 2025-05-03 12:40:26 +00:00
commit ae94bd9631
11 changed files with 75 additions and 59 deletions

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { computedAsync } from '@vueuse/core';
import { useCookie, useFetch } from 'nuxt/app';
import useConfig from '../composables/useConfig.ts';
@ -41,6 +42,10 @@ const username = ref(user.value.username);
const email = ref(user.value.email);
const socialLookup = ref(Boolean(user.value.socialLookup));
const gravatarSrc = computedAsync(async () => {
return user.value ? await gravatar(user.value) : undefined;
});
const message = ref('');
const messageParams = ref({});
const messageIcon = ref<string | null>(null);
@ -319,7 +324,7 @@ const addBrackets = (str: string): string => {
<div v-else class="mt-3">
Gravatar:
<a href="#" @click.prevent="setAvatar('gravatar')">
<Avatar :user="user" :src="gravatar(user)" dsize="2rem" />
<Avatar :user="user" :src="gravatarSrc" dsize="2rem" />
</a>
</div>
<div v-if="user.avatarSource">

View File

@ -1,7 +1,41 @@
<script setup lang="ts">
import { computedAsync } from '@vueuse/core';
import { fallbackAvatar, gravatar } from '~/src/helpers.ts';
import type { User } from '~/src/user.ts';
interface AvatarUser extends Pick<User, 'username' | 'emailHash'> {
email?: User['email'];
avatar?: string;
avatarSource?: User['avatarSource'];
}
const props = withDefaults(defineProps<{
user: AvatarUser;
src?: string;
size?: number;
dsize?: string;
validate?: boolean;
}>(), {
size: 128,
dsize: '6rem',
});
const failedToLoad = ref(false);
const resolvedSrc = computedAsync(async () => {
return props.src ||
props.user.avatar ||
(props.user.avatarSource === 'gravatar' && props.user.email !== undefined
? await gravatar(props.user as AvatarUser & Required<Pick<AvatarUser, 'email'>>, props.size)
: fallbackAvatar(props.user, props.size));
});
</script>
<template>
<span>
<img
:src="src || user.avatar || (user.avatarSource === 'gravatar' ? gravatar(user, size) : fallbackAvatar(user, size))"
:src="resolvedSrc"
alt=""
class="rounded-circle"
:style="`width: ${dsize};height: ${dsize};`"
@ -11,27 +45,6 @@
</span>
</template>
<script>
import { fallbackAvatar, gravatar } from '../src/helpers.ts';
export default {
props: {
user: { required: true },
src: {},
size: { default: 128 },
dsize: { default: '6rem' },
validate: { type: Boolean },
},
data() {
return {
fallbackAvatar,
gravatar,
failedToLoad: false,
};
},
};
</script>
<style lang="scss" scoped>
.failed-to-load {
max-width: 200px;

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import sorter from 'avris-sorter';
import md5 from 'js-md5';
import { useCookie } from 'nuxt/app';
import useConfig from '~/composables/useConfig.ts';
@ -8,11 +7,11 @@ import useDark from '~/composables/useDark.ts';
import useDialogue from '~/composables/useDialogue.ts';
import { longtimeCookieSetting } from '~/src/cookieSettings.ts';
import { LoadScriptError } from '~/src/errors.ts';
import { executeUnlessPrerendering } from '~/src/helpers.ts';
import { executeUnlessPrerendering, sha256 } from '~/src/helpers.ts';
import { useMainStore } from '~/store/index.ts';
// no need to be super secure, just a sign that the page is not public
const TESTER_PASSWORD_HASH = '82feeb96d60170e714df8fb062301e90';
const TESTER_PASSWORD_HASH = '3754c03824c4ea3b13e7b4b2d7ad35992dbf64f9ee680493fc7093bf176f49e5';
declare global {
interface Window {
@ -35,8 +34,12 @@ const requiresLogin = computed((): boolean => {
return !config.macrolanguage?.enabled && (runtimeConfig.public.env === 'test' ||
config.locale !== '_' && !locales[config.locale]?.published);
});
const testerPasswordValid = computed((): boolean => {
return !!testerPasswordCookie.value && md5(testerPasswordCookie.value) === TESTER_PASSWORD_HASH;
const testerPasswordValid = ref<boolean | undefined>();
watchEffect(async () => {
testerPasswordValid.value = undefined;
if (testerPasswordCookie.value) {
testerPasswordValid.value = await sha256(testerPasswordCookie.value) === TESTER_PASSWORD_HASH;
}
});
const checkTesterPassword = (): void => {
testerPasswordCookie.value = testerPassword.value;
@ -123,7 +126,7 @@ const loadAds = async (): Promise<void> => {
Sign in
</button>
</div>
<p v-if="testerPasswordCookie && !testerPasswordValid" class="small text-danger">
<p v-if="testerPasswordCookie && testerPasswordValid === false" class="small text-danger">
<Icon v="exclamation-triangle" />
Password invalid
</p>

View File

@ -34,7 +34,6 @@
"ics": "^3.7.6",
"jose": "^5.9.6",
"js-base64": "^3.5.2",
"js-md5": "^0.7.3",
"jsdom": "^26.0.0",
"luxon": "^1.28.1",
"mastodon": "^1.2.2",

8
pnpm-lock.yaml generated
View File

@ -62,9 +62,6 @@ importers:
js-base64:
specifier: ^3.5.2
version: 3.7.7
js-md5:
specifier: ^0.7.3
version: 0.7.3
jsdom:
specifier: ^26.0.0
version: 26.0.0(canvas@3.1.0)
@ -5521,9 +5518,6 @@ packages:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-md5@0.7.3:
resolution: {integrity: sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -15118,8 +15112,6 @@ snapshots:
js-cookie@3.0.5: {}
js-md5@0.7.3: {}
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}

View File

@ -17,7 +17,7 @@ export default async (
if (user.avatarSource && process.env.NUXT_PUBLIC_CLOUDFRONT && user.avatarSource.startsWith(process.env.NUXT_PUBLIC_CLOUDFRONT!)) {
return user.avatarSource;
} else if (user.avatarSource === 'gravatar') {
return gravatar(user);
return await gravatar(user);
} else if (user.avatarSource) {
if (user.payload) {
return fromPayload(user.payload);

View File

@ -64,7 +64,7 @@ const run = async (config: Config, baseUrl: string): Promise<void> => {
const prev = fs.existsSync(prevPath) ? JSON.parse(fs.readFileSync(prevPath, 'utf-8')) : {};
const localEvents = (await import(`../locale/${config.locale}/calendar/events.ts`)).default;
const current = buildCalendar(localEvents, process.env.NUXT_PUBLIC_BASE_URL!).buildSummary();
const current = await buildCalendar(localEvents, process.env.NUXT_PUBLIC_BASE_URL!).buildSummary();
const changedYears = new Set();
for (const day of Object.keys(current)) {
const year = day.substring(0, 4);

View File

@ -6,7 +6,6 @@ import * as Sentry from '@sentry/node';
import { Router } from 'express';
import type { Request, Response } from 'express';
import { getH3Event } from 'h3-express';
import md5 from 'js-md5';
import type { ParsedQs } from 'qs';
import SQL from 'sql-template-strings';
import { ulid } from 'ulid';
@ -19,6 +18,7 @@ import {
now,
isValidLink,
newDate,
sha256,
} from '../../src/helpers.ts';
import { normaliseUrl } from '../../src/links.ts';
import type { Opinion } from '../../src/opinions.ts';
@ -524,7 +524,7 @@ const fetchProfilesRoute = async (req: Request, res: Response, locale: string, u
});
}
user.emailHash = md5(user.email);
user.emailHash = await sha256(user.email);
delete user.email;
user.avatar = await avatar(req.db, user);

View File

@ -1,10 +1,8 @@
import { S3 } from '@aws-sdk/client-s3';
import { loadImage, createCanvas } from 'canvas';
import md5 from 'js-md5';
import { newDate } from '../src/helpers.ts';
import { awsConfig, awsParams } from './aws.ts';
import { awsConfig, awsParams } from '~/server/aws.ts';
import { newDate, sha256 } from '~/src/helpers.ts';
const s3 = new S3(awsConfig);
@ -23,7 +21,7 @@ export default async (prefix: string, url: string, ttlDays: number | null = null
const isSvg = url.toLowerCase().replace(/\?.*$/, '').endsWith('.svg') || url.startsWith('data:image/svg+xml');
const key = `${prefix}/${md5(url)}.${isSvg ? 'svg' : 'png'}`;
const key = `${prefix}/${await sha256(url)}.${isSvg ? 'svg' : 'png'}`;
try {
const metadata = await s3.headObject({ Key: key, ...awsParams });

View File

@ -1,10 +1,8 @@
import type { EventAttributes } from 'ics';
import md5 from 'js-md5';
import nepali from 'nepali-calendar-js';
import { v5 as uuid5 } from 'uuid';
import { newDate } from '../helpers.ts';
import { newDate, sha256 } from '~/src/helpers.ts';
import type { Translator } from '~/src/translator.ts';
export class Day {
@ -448,16 +446,13 @@ export class Calendar {
}
}
buildSummary(): Record<string, string> {
async buildSummary(): Promise<Record<string, string>> {
const summary: Record<string, string> = {};
for (const year of this.getAllYears()) {
for (let month = 1; month <= 12; month++) {
for (const day of iterateMonth(year.year, month)) {
const events = [];
for (const event of year.eventsByDate[day.toString()] || []) {
events.push(event.name);
}
summary[day.toString()] = md5(JSON.stringify(events));
const events = (year.eventsByDate[day.toString()] || []).map((event) => event.name);
summary[day.toString()] = await sha256(JSON.stringify(events));
}
}
}

View File

@ -2,7 +2,6 @@ import * as Sentry from '@sentry/browser';
import type { Request, Response, NextFunction } from 'express';
import { importSPKI, jwtVerify } from 'jose';
import { Base64 } from 'js-base64';
import md5 from 'js-md5';
import type { Database } from '../server/db.ts';
@ -148,8 +147,20 @@ export const fallbackAvatar = (user: Pick<User, 'username'>, size: number = 240)
.replace(/\//g, '_')}.png`;
};
export const gravatar = (user: Pick<User, 'username' | 'email'> & { emailHash?: string }, size: number = 240): string => {
return `https://www.gravatar.com/avatar/${user.emailHash || md5(user.email)}?d=${encodeURIComponent(fallbackAvatar(user, size))}&s=${size}`;
export const sha256 = async (message: string) => {
const encoded = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('sha-256', encoded);
return Array.from(new Uint8Array(hashBuffer))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
};
export const gravatar = async (
user: Pick<User, 'username' | 'email'> & { emailHash?: string },
size: number = 240,
): Promise<string> => {
const emailHash = user.emailHash ?? await sha256(user.email);
return `https://gravatar.com/avatar/${emailHash}?d=${encodeURIComponent(fallbackAvatar(user, size))}&s=${size}`;
};
export interface DictEntry<K extends string | number | symbol, V> {