feat: add user role field and implement Gravatar fallback for user avatars

This commit is contained in:
Jose Selesan
2026-04-16 20:51:53 -03:00
parent e5002ce440
commit 34a24ce6c9
4 changed files with 86 additions and 4 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "role" TEXT NOT NULL DEFAULT 'member';

View File

@@ -1,6 +1,5 @@
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
// If your Prisma file is located elsewhere, you can change the path
import { openAPI } from 'better-auth/plugins';
import { db } from './prisma';
@@ -19,7 +18,7 @@ export const auth = betterAuth({
enabled: true,
},
trustedOrigins: [process.env.APP_BASE_URL ?? 'http://localhost:5173'],
plugins: [],
plugins: [openAPI()],
});
export type Session = typeof auth.$Infer.Session.session;

View File

@@ -1,13 +1,19 @@
import { createHash } from 'node:crypto';
import type { User } from '@/lib/auth';
import type { UserProfile } from '@repo/api-contract';
function getGravatarUrl(email: string): string {
const hash = createHash('md5').update(email.trim().toLowerCase()).digest('hex');
return `https://www.gravatar.com/avatar/${hash}?d=identicon`;
}
export function getUserProfile(user: User): UserProfile {
return {
id: user.id,
fullName: user.name,
email: user.email,
role: (user as any).role || 'member',
avatarUrl: user.image || null,
avatarUrl: user.image || getGravatarUrl(user.email),
createdAt: user.createdAt,
};
}

View File

@@ -40,6 +40,80 @@ export type AuthContextValue = {
const AuthContext = createContext<AuthContextValue | null>(null);
/**
* Standard MD5 implementation for Gravatar support
* (Self-contained to avoid dependency issues)
*/
function md5(string: string) {
const k = Array.from({ length: 64 }, (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32));
let a = 0x67452301;
let b = 0xefcdab89;
let c = 0x98badcfe;
let d = 0x10325476;
const s = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14,
20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10,
15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
];
const str = unescape(encodeURIComponent(string));
const msg = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) msg[i] = str.charCodeAt(i);
const len = msg.length;
const wordLen = ((len + 8) >> 6) + 1;
const words = new Uint32Array(wordLen * 16);
for (let i = 0; i < len; i++) words[i >> 2] |= msg[i] << ((i % 4) * 8);
words[len >> 2] |= 0x80 << ((len % 4) * 8);
words[wordLen * 16 - 2] = len * 8;
for (let i = 0; i < words.length; i += 16) {
let [aa, bb, cc, dd] = [a, b, c, d];
for (let j = 0; j < 64; j++) {
let f: number;
let g: number;
if (j < 16) {
f = (b & c) | (~b & d);
g = j;
} else if (j < 32) {
f = (d & b) | (~d & c);
g = (5 * j + 1) % 16;
} else if (j < 48) {
f = b ^ c ^ d;
g = (3 * j + 5) % 16;
} else {
f = c ^ (b | ~d);
g = (7 * j) % 16;
}
const temp = d;
d = c;
c = b;
b =
(b +
((a + f + k[j] + words[i + g]) << s[j] | (a + f + k[j] + words[i + g]) >>> (32 - s[j]))) |
0;
a = temp;
}
a = (a + aa) | 0;
b = (b + bb) | 0;
c = (c + cc) | 0;
d = (d + dd) | 0;
}
return [a, b, c, d]
.map((v) =>
Array.from({ length: 4 }, (_, i) => ((v >> (i * 8)) & 0xff).toString(16).padStart(2, '0')).join(
''
)
)
.join('');
}
function getGravatarUrl(email: string): string {
const hash = md5(email.trim().toLowerCase());
return `https://www.gravatar.com/avatar/${hash}?d=identicon`;
}
function getDisplayName(user: User | null): string {
if (!user) return 'Usuario';
if (user.name?.trim()) return user.name.trim();
@@ -53,7 +127,8 @@ function getInitials(name: string): string {
}
function getAvatarUrl(user: User | null): string | null {
return user?.image || null;
if (!user) return null;
return user.image || getGravatarUrl(user.email);
}
export function AuthProvider({ children }: PropsWithChildren) {