feat: Add user profile photo upload with base64 encoding
Added complete photo upload support for user profiles: - Added photoUrl field to User entity and all auth responses - Created migration V008 for photo_url column in users table - Updated UpdateProfileDto to include photoUrl validation - Modified auth.service.ts to handle photoUrl in all user responses (register, login, getUserById, updateProfile, refreshAccessToken, loginWithExternalAuth) - Updated AuthResponse interface to include photoUrl - Updated frontend User and UserProfile interfaces to include photoUrl - Updated AppShell to display user photoUrl in header avatar - Fixed ChildDialog to remove invalid props from PhotoUpload component The photo upload uses base64 encoding (max 5MB) for simplicity and works across all environments. Future enhancement can migrate to CDN/object storage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,7 @@ export class AuthService {
|
|||||||
id: savedUser.id,
|
id: savedUser.id,
|
||||||
email: savedUser.email,
|
email: savedUser.email,
|
||||||
name: savedUser.name,
|
name: savedUser.name,
|
||||||
|
photoUrl: savedUser.photoUrl,
|
||||||
locale: savedUser.locale,
|
locale: savedUser.locale,
|
||||||
emailVerified: savedUser.emailVerified,
|
emailVerified: savedUser.emailVerified,
|
||||||
preferences: savedUser.preferences,
|
preferences: savedUser.preferences,
|
||||||
@@ -234,6 +235,7 @@ export class AuthService {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
locale: user.locale,
|
locale: user.locale,
|
||||||
emailVerified: user.emailVerified,
|
emailVerified: user.emailVerified,
|
||||||
preferences: user.preferences,
|
preferences: user.preferences,
|
||||||
@@ -299,6 +301,7 @@ export class AuthService {
|
|||||||
id: refreshToken.user.id,
|
id: refreshToken.user.id,
|
||||||
email: refreshToken.user.email,
|
email: refreshToken.user.email,
|
||||||
name: refreshToken.user.name,
|
name: refreshToken.user.name,
|
||||||
|
photoUrl: refreshToken.user.photoUrl,
|
||||||
locale: refreshToken.user.locale,
|
locale: refreshToken.user.locale,
|
||||||
emailVerified: refreshToken.user.emailVerified,
|
emailVerified: refreshToken.user.emailVerified,
|
||||||
preferences: refreshToken.user.preferences,
|
preferences: refreshToken.user.preferences,
|
||||||
@@ -361,6 +364,7 @@ export class AuthService {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
locale: user.locale,
|
locale: user.locale,
|
||||||
timezone: user.timezone,
|
timezone: user.timezone,
|
||||||
@@ -375,6 +379,7 @@ export class AuthService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
updateData: {
|
updateData: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
photoUrl?: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
preferences?: {
|
preferences?: {
|
||||||
notifications?: boolean;
|
notifications?: boolean;
|
||||||
@@ -407,6 +412,12 @@ export class AuthService {
|
|||||||
this.logger.log(`Updated user.name to: "${user.name}"`);
|
this.logger.log(`Updated user.name to: "${user.name}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update photo URL if provided
|
||||||
|
if (updateData.photoUrl !== undefined) {
|
||||||
|
user.photoUrl = updateData.photoUrl;
|
||||||
|
this.logger.log(`Updated user.photoUrl (length: ${updateData.photoUrl?.length || 0})`);
|
||||||
|
}
|
||||||
|
|
||||||
// Update timezone if provided
|
// Update timezone if provided
|
||||||
if (updateData.timezone !== undefined) {
|
if (updateData.timezone !== undefined) {
|
||||||
user.timezone = updateData.timezone;
|
user.timezone = updateData.timezone;
|
||||||
@@ -431,6 +442,7 @@ export class AuthService {
|
|||||||
id: updatedUser.id,
|
id: updatedUser.id,
|
||||||
email: updatedUser.email,
|
email: updatedUser.email,
|
||||||
name: updatedUser.name,
|
name: updatedUser.name,
|
||||||
|
photoUrl: updatedUser.photoUrl,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
locale: updatedUser.locale,
|
locale: updatedUser.locale,
|
||||||
timezone: updatedUser.timezone,
|
timezone: updatedUser.timezone,
|
||||||
@@ -556,6 +568,7 @@ export class AuthService {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
locale: user.locale,
|
locale: user.locale,
|
||||||
emailVerified: user.emailVerified,
|
emailVerified: user.emailVerified,
|
||||||
preferences: user.preferences,
|
preferences: user.preferences,
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export class UpdateProfileDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
photoUrl?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface AuthResponse {
|
|||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
photoUrl?: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
families?: Array<{ id: string; familyId: string; role: string }>;
|
families?: Array<{ id: string; familyId: string; role: string }>;
|
||||||
|
|||||||
@@ -153,8 +153,6 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
|
|||||||
onChange={(url) => setFormData({ ...formData, photoUrl: url })}
|
onChange={(url) => setFormData({ ...formData, photoUrl: url })}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
size={80}
|
size={80}
|
||||||
childId={child?.id}
|
|
||||||
type="profile"
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export const AppShell = ({ children }: AppShellProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
src={user?.photoUrl}
|
||||||
sx={{
|
sx={{
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface UserProfile {
|
|||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
photoUrl?: string;
|
||||||
role: string;
|
role: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface User {
|
|||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
photoUrl?: string;
|
||||||
role: string;
|
role: string;
|
||||||
families?: Array<{
|
families?: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user