feat: add user role field and implement Gravatar fallback for user avatars
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "role" TEXT NOT NULL DEFAULT 'member';
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { betterAuth } from 'better-auth';
|
import { betterAuth } from 'better-auth';
|
||||||
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
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 { openAPI } from 'better-auth/plugins';
|
||||||
import { db } from './prisma';
|
import { db } from './prisma';
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ export const auth = betterAuth({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
trustedOrigins: [process.env.APP_BASE_URL ?? 'http://localhost:5173'],
|
trustedOrigins: [process.env.APP_BASE_URL ?? 'http://localhost:5173'],
|
||||||
plugins: [],
|
plugins: [openAPI()],
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Session = typeof auth.$Infer.Session.session;
|
export type Session = typeof auth.$Infer.Session.session;
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
import type { User } from '@/lib/auth';
|
import type { User } from '@/lib/auth';
|
||||||
import type { UserProfile } from '@repo/api-contract';
|
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 {
|
export function getUserProfile(user: User): UserProfile {
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
fullName: user.name,
|
fullName: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: (user as any).role || 'member',
|
role: (user as any).role || 'member',
|
||||||
avatarUrl: user.image || null,
|
avatarUrl: user.image || getGravatarUrl(user.email),
|
||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,80 @@ export type AuthContextValue = {
|
|||||||
|
|
||||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
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 {
|
function getDisplayName(user: User | null): string {
|
||||||
if (!user) return 'Usuario';
|
if (!user) return 'Usuario';
|
||||||
if (user.name?.trim()) return user.name.trim();
|
if (user.name?.trim()) return user.name.trim();
|
||||||
@@ -53,7 +127,8 @@ function getInitials(name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAvatarUrl(user: User | null): string | null {
|
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) {
|
export function AuthProvider({ children }: PropsWithChildren) {
|
||||||
|
|||||||
Reference in New Issue
Block a user