Edit on GitHub

User Data and Profiles

Managing user data and profiles is an important aspect of any application with authentication. This guide covers how to work with user data in Svelte Guardian, including creating and updating user profiles.

User Data Structure

Svelte Guardian stores core user data in the User table/collection with the following standard fields:

  • id: Unique identifier
  • email: User’s email address
  • emailVerified: Timestamp when email was verified
  • name: User’s display name (optional)
  • image: URL to user’s avatar image (optional)
  • password: Hashed password (for credentials provider)
  • role: User role for authorization

Extending the User Model

Using Prisma

When using Prisma, extend the User model in schema.prisma:

model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? password String? image String? role String? @default("user") accounts Account[] sessions Session[] // Custom fields firstName String? lastName String? birthDate DateTime? phoneNumber String? address Address? preferences Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Address { id String @id @default(cuid()) street String city String state String? country String zipCode String userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) }

After modifying your schema, run a migration:

npx prisma migrate dev --name add_user_profile_fields

Using MongoDB

With MongoDB, the schema is more flexible:

interface ExtendedUser { _id: string; name?: string; email?: string; emailVerified?: Date; image?: string; role?: string; // Custom fields firstName?: string; lastName?: string; birthDate?: Date; phoneNumber?: string; address?: { street: string; city: string; state?: string; country: string; zipCode: string; }; preferences?: Record; createdAt: Date; updatedAt: Date; }

Accessing User Data

Server-Side Access

Access user data in server code:

// src/routes/profile/+page.server.ts import type { PageServerLoad } from './$types'; import { prisma } from '$lib/database'; export const load: PageServerLoad = async ({ locals }) => { const session = await locals.getSession(); if (!session?.user) { return { user: null }; } // Fetch extended user data from database const userData = await prisma.user.findUnique({ where: { id: session.user.id }, include: { address: true } }); return { user: { ...session.user, ...userData, // Don't expose sensitive data password: undefined } }; };

Client-Side Access

Use the data in client components:

{#if user}

Your Profile

{#if user.image} Profile {/if}

{user.name || user.email}

{user.role}

Personal Information

Email: {user.email}

First Name: {user.firstName || 'Not set'}

Last Name: {user.lastName || 'Not set'}

Phone: {user.phoneNumber || 'Not set'}

Birth Date: {user.birthDate ? new Date(user.birthDate).toLocaleDateString() : 'Not set'}

{#if user.address}

Address

{user.address.street}

{user.address.city}, {user.address.state || ''} {user.address.zipCode}

{user.address.country}

{/if}
{:else}

Please sign in to view your profile.

{/if}

Profile Editing

Create a profile editing form:

Edit Profile

{#if form?.error}
{form.error}
{/if} {#if form?.success}
Profile updated successfully!
{/if}
Cancel

And create the server action to handle profile updates:

// src/routes/profile/edit/+page.server.ts import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { prisma } from '$lib/database'; export const load: PageServerLoad = async ({ locals }) => { const session = await locals.getSession(); if (!session?.user) { redirect(302, '/signin'); } // Fetch user data with address const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { address: true } }); return { user }; }; export const actions = { default: async ({ request, locals }) => { const session = await locals.getSession(); if (!session?.user) { return fail(401, { error: 'Unauthorized' }); } const formData = await request.formData(); const firstName = formData.get('firstName')?.toString() || ''; const lastName = formData.get('lastName')?.toString() || ''; const phoneNumber = formData.get('phoneNumber')?.toString() || ''; const birthDate = formData.get('birthDate')?.toString() || ''; const street = formData.get('street')?.toString() || ''; const city = formData.get('city')?.toString() || ''; const state = formData.get('state')?.toString() || ''; const country = formData.get('country')?.toString() || ''; const zipCode = formData.get('zipCode')?.toString() || ''; try { // Update user data await prisma.user.update({ where: { id: session.user.id }, data: { firstName, lastName, phoneNumber, birthDate: birthDate ? new Date(birthDate) : null, name: `${firstName} ${lastName}`.trim() || undefined, // Upsert address - create if doesn't exist, update if it does address: { upsert: { create: { street, city, state, country, zipCode }, update: { street, city, state, country, zipCode } } } } }); return { success: true }; } catch (error) { console.error('Error updating profile:', error); return fail(500, { error: 'Failed to update profile' }); } } } satisfies Actions;

Avatar/Profile Image Management

Add functionality for users to upload profile images:

Update Profile Picture

{#if form?.error}
{form.error}
{/if} {#if form?.success}
Profile picture updated successfully!
{/if}
{#if previewUrl} Profile Preview {:else}
No Image
{/if}
Max size: 2MB. Recommended: 500x500px square image.
Cancel

Handle the image upload server-side:

// src/routes/profile/avatar/+page.server.ts import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { prisma } from '$lib/database'; import { uploadImage } from '$lib/storage'; // Implement this based on your storage solution export const load: PageServerLoad = async ({ locals }) => { const session = await locals.getSession(); if (!session?.user) { redirect(302, '/signin'); } return { user: session.user }; }; export const actions = { default: async ({ request, locals }) => { const session = await locals.getSession(); if (!session?.user) { return fail(401, { error: 'Unauthorized' }); } const formData = await request.formData(); const avatarFile = formData.get('avatar'); if (!avatarFile || !(avatarFile instanceof File)) { return fail(400, { error: 'No valid file uploaded' }); } if (avatarFile.size > 2 * 1024 * 1024) { return fail(400, { error: 'File size exceeds 2MB limit' }); } try { // Upload image to storage (implement this based on your storage solution) const imageUrl = await uploadImage( avatarFile, `users/${session.user.id}/avatar`, { width: 500, height: 500 } ); // Update user record with new image URL await prisma.user.update({ where: { id: session.user.id }, data: { image: imageUrl } }); return { success: true }; } catch (error) { console.error('Error uploading avatar:', error); return fail(500, { error: 'Failed to upload image' }); } } } satisfies Actions;

Changing Password

Create a password change form:

Change Password

{#if form?.error}
{form.error}
{/if} {#if form?.success}
Password changed successfully!
{/if}
{ if (!validateForm()) { form = { error: 'New passwords do not match' }; return; } e.target.submit(); }} >
Password must be at least 8 characters long
Cancel

And the server handler:

// src/routes/profile/change-password/+page.server.ts import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { changePassword } from '$lib/auth'; export const load: PageServerLoad = async ({ locals }) => { const session = await locals.getSession(); if (!session?.user) { redirect(302, '/signin'); } return {}; }; export const actions = { default: async ({ request, locals }) => { const session = await locals.getSession(); if (!session?.user) { return fail(401, { error: 'Unauthorized' }); } const formData = await request.formData(); const currentPassword = formData.get('currentPassword')?.toString() || ''; const newPassword = formData.get('newPassword')?.toString() || ''; const confirmPassword = formData.get('confirmPassword')?.toString() || ''; if (newPassword !== confirmPassword) { return fail(400, { error: 'New passwords do not match' }); } if (newPassword.length < 8) { return fail(400, { error: 'New password must be at least 8 characters long' }); } try { // Use the Svelte Guardian utility to change password await changePassword({ userId: session.user.id, currentPassword, newPassword }); return { success: true }; } catch (error) { console.error('Error changing password:', error); if (error.code === 'INVALID_PASSWORD') { return fail(400, { error: 'Current password is incorrect' }); } return fail(500, { error: 'Failed to change password' }); } } } satisfies Actions;

Best Practices

  1. Data Validation: Always validate user input on both client and server
  2. Security: Never expose sensitive information like hashed passwords
  3. Incremental Loading: For complex profiles, load data incrementally
  4. Atomic Updates: Update specific parts of the profile independently
  5. Error Handling: Provide clear feedback when updates fail
  6. Data Privacy: Allow users to download or delete their data
  7. Image Optimization: Resize and compress profile images
  8. Performance: Use database indexes for efficient user lookup

Next Steps

After implementing user profiles, consider these enhancements:

Share this page