diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py
index 069865036..a97312ff9 100644
--- a/backend/apps/web/models/auths.py
+++ b/backend/apps/web/models/auths.py
@@ -86,6 +86,7 @@ class SignupForm(BaseModel):
name: str
email: str
password: str
+ profile_image_url: Optional[str] = "/user.png"
class AuthsTable:
@@ -94,7 +95,12 @@ class AuthsTable:
self.db.create_tables([Auth])
def insert_new_auth(
- self, email: str, password: str, name: str, role: str = "pending"
+ self,
+ email: str,
+ password: str,
+ name: str,
+ profile_image_url: str = "/user.png",
+ role: str = "pending",
) -> Optional[UserModel]:
log.info("insert_new_auth")
@@ -105,7 +111,7 @@ class AuthsTable:
)
result = Auth.create(**auth.model_dump())
- user = Users.insert_new_user(id, name, email, role)
+ user = Users.insert_new_user(id, name, email, profile_image_url, role)
if result and user:
return user
diff --git a/backend/apps/web/models/users.py b/backend/apps/web/models/users.py
index a01e595e5..7d1e182da 100644
--- a/backend/apps/web/models/users.py
+++ b/backend/apps/web/models/users.py
@@ -31,7 +31,7 @@ class UserModel(BaseModel):
name: str
email: str
role: str = "pending"
- profile_image_url: str = "/user.png"
+ profile_image_url: str
timestamp: int # timestamp in epoch
api_key: Optional[str] = None
@@ -59,7 +59,12 @@ class UsersTable:
self.db.create_tables([User])
def insert_new_user(
- self, id: str, name: str, email: str, role: str = "pending"
+ self,
+ id: str,
+ name: str,
+ email: str,
+ profile_image_url: str = "/user.png",
+ role: str = "pending",
) -> Optional[UserModel]:
user = UserModel(
**{
@@ -67,7 +72,7 @@ class UsersTable:
"name": name,
"email": email,
"role": role,
- "profile_image_url": "/user.png",
+ "profile_image_url": profile_image_url,
"timestamp": int(time.time()),
}
)
diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py
index 293cb55b8..89d8c1c8f 100644
--- a/backend/apps/web/routers/auths.py
+++ b/backend/apps/web/routers/auths.py
@@ -163,7 +163,11 @@ async def signup(request: Request, form_data: SignupForm):
)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
- form_data.email.lower(), hashed, form_data.name, role
+ form_data.email.lower(),
+ hashed,
+ form_data.name,
+ form_data.profile_image_url,
+ role,
)
if user:
diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts
index 548a9418d..efeeff333 100644
--- a/src/lib/apis/auths/index.ts
+++ b/src/lib/apis/auths/index.ts
@@ -58,7 +58,12 @@ export const userSignIn = async (email: string, password: string) => {
return res;
};
-export const userSignUp = async (name: string, email: string, password: string) => {
+export const userSignUp = async (
+ name: string,
+ email: string,
+ password: string,
+ profile_image_url: string
+) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
@@ -69,7 +74,8 @@ export const userSignUp = async (name: string, email: string, password: string)
body: JSON.stringify({
name: name,
email: email,
- password: password
+ password: password,
+ profile_image_url: profile_image_url
})
})
.then(async (res) => {
diff --git a/src/lib/components/chat/Settings/Account.svelte b/src/lib/components/chat/Settings/Account.svelte
index e0239fb0b..7982bff28 100644
--- a/src/lib/components/chat/Settings/Account.svelte
+++ b/src/lib/components/chat/Settings/Account.svelte
@@ -7,6 +7,7 @@
import UpdatePassword from './Account/UpdatePassword.svelte';
import { getGravatarUrl } from '$lib/apis/utils';
+ import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
import { copyToClipboard } from '$lib/utils';
import Plus from '$lib/components/icons/Plus.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -28,6 +29,12 @@
let profileImageInputElement: HTMLInputElement;
const submitHandler = async () => {
+ if (name !== $user.name) {
+ if (profileImageUrl === generateInitialsImage($user.name) || profileImageUrl === '') {
+ profileImageUrl = generateInitialsImage(name);
+ }
+ }
+
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
(error) => {
toast.error(error);
@@ -125,11 +132,11 @@
}}
/>
-
{$i18n.t('Profile')}
-
-
+
+
+
+
+
+
+
+
+
+
+
{$i18n.t('Profile Image')}
+
+
+
+
+
+
+
+
+
+
{$i18n.t('Name')}
+
+
+
+
+
+
diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json
index 8c8e5dd27..e11ef0131 100644
--- a/src/lib/i18n/locales/en-US/translation.json
+++ b/src/lib/i18n/locales/en-US/translation.json
@@ -150,6 +150,7 @@
"Failed to read clipboard contents": "",
"File Mode": "",
"File not found.": "",
+ "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "",
"Focus chat input": "",
"Format your variables using square brackets like this:": "",
"From (Base Model)": "",
@@ -340,6 +341,7 @@
"URL Mode": "",
"Use '#' in the prompt input to load and select your documents.": "",
"Use Gravatar": "",
+ "Use Initials": "",
"user": "",
"User Permissions": "",
"Users": "",
diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts
index 9ee8b6fc1..e9a4e229b 100644
--- a/src/lib/utils/index.ts
+++ b/src/lib/utils/index.ts
@@ -111,6 +111,82 @@ export const getGravatarURL = (email) => {
return `https://www.gravatar.com/avatar/${hash}`;
};
+export const canvasPixelTest = () => {
+ // Test a 1x1 pixel to potentially identify browser/plugin fingerprint blocking or spoofing
+ // Inspiration: https://github.com/kkapsner/CanvasBlocker/blob/master/test/detectionTest.js
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ canvas.height = 1;
+ canvas.width = 1;
+ const imageData = new ImageData(canvas.width, canvas.height);
+ const pixelValues = imageData.data;
+
+ // Generate RGB test data
+ for (let i = 0; i < imageData.data.length; i += 1) {
+ if (i % 4 !== 3) {
+ pixelValues[i] = Math.floor(256 * Math.random());
+ } else {
+ pixelValues[i] = 255;
+ }
+ }
+
+ ctx.putImageData(imageData, 0, 0);
+ const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+
+ // Read RGB data and fail if unmatched
+ for (let i = 0; i < p.length; i += 1) {
+ if (p[i] !== pixelValues[i]) {
+ console.log(
+ 'canvasPixelTest: Wrong canvas pixel RGB value detected:',
+ p[i],
+ 'at:',
+ i,
+ 'expected:',
+ pixelValues[i]
+ );
+ console.log('canvasPixelTest: Canvas blocking or spoofing is likely');
+ return false;
+ }
+ }
+
+ return true;
+};
+
+export const generateInitialsImage = (name) => {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ canvas.width = 100;
+ canvas.height = 100;
+
+ if (!canvasPixelTest()) {
+ console.log(
+ 'generateInitialsImage: failed pixel test, fingerprint evasion is likely. Using default image.'
+ );
+ return '/user.png';
+ }
+
+ ctx.fillStyle = '#F39C12';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ ctx.fillStyle = '#FFFFFF';
+ ctx.font = '40px Helvetica';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ const sanitizedName = name.trim();
+ const initials =
+ sanitizedName.length > 0
+ ? sanitizedName[0] +
+ (sanitizedName.split(' ').length > 1
+ ? sanitizedName[sanitizedName.lastIndexOf(' ') + 1]
+ : '')
+ : '';
+
+ ctx.fillText(initials.toUpperCase(), canvas.width / 2, canvas.height / 2);
+
+ return canvas.toDataURL();
+};
+
export const copyToClipboard = (text) => {
if (!navigator.clipboard) {
const textArea = document.createElement('textarea');
diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte
index 2dc2a92b2..c7eab0db8 100644
--- a/src/routes/auth/+page.svelte
+++ b/src/routes/auth/+page.svelte
@@ -6,6 +6,7 @@
import { WEBUI_NAME, config, user } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
+ import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
const i18n = getContext('i18n');
@@ -36,10 +37,12 @@
};
const signUpHandler = async () => {
- const sessionUser = await userSignUp(name, email, password).catch((error) => {
- toast.error(error);
- return null;
- });
+ const sessionUser = await userSignUp(name, email, password, generateInitialsImage(name)).catch(
+ (error) => {
+ toast.error(error);
+ return null;
+ }
+ );
await setSessionUser(sessionUser);
};