Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spotty-months-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

Add `updateUserProfileImage` and `updateOrganizationLogo` methods for uploading images to `User` and `Organization` respectively.
Comment thread
anagstef marked this conversation as resolved.
29 changes: 29 additions & 0 deletions packages/backend/src/api/endpoints/OrganizationApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import runtime from '../../runtime';
import { joinPaths } from '../../util/path';
import type { Organization, OrganizationInvitation, OrganizationMembership } from '../resources';
import type { OrganizationMembershipRole } from '../resources/Enums';
Expand Down Expand Up @@ -33,6 +34,11 @@ type UpdateParams = {
maxAllowedMemberships?: number;
} & MetadataParams;

type UpdateLogoParams = {
file: Blob | File;
uploaderUserId: string;
};

type UpdateMetadataParams = MetadataParams;

type GetOrganizationMembershipListParams = {
Expand Down Expand Up @@ -116,6 +122,29 @@ export class OrganizationAPI extends AbstractAPI {
});
}

public async updateOrganizationLogo(organizationId: string, params: UpdateLogoParams) {
Comment thread
gkats marked this conversation as resolved.
this.requireId(organizationId);

const formData = new runtime.FormData();
formData.append('file', params?.file);
formData.append('uploader_user_id', params?.uploaderUserId);

return this.request<Organization>({
method: 'PUT',
path: joinPaths(basePath, organizationId, 'logo'),
formData,
});
}

public async deleteOrganizationLogo(organizationId: string) {
this.requireId(organizationId);

return this.request<Organization>({
method: 'DELETE',
path: joinPaths(basePath, organizationId, 'logo'),
});
}

public async updateOrganizationMetadata(organizationId: string, params: UpdateMetadataParams) {
this.requireId(organizationId);

Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/api/endpoints/UserApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { OAuthProvider } from '@clerk/types';

import runtime from '../../runtime';
import { joinPaths } from '../../util/path';
import type { OauthAccessToken, OrganizationMembership, User } from '../resources';
import { AbstractAPI } from './AbstractApi';
Expand Down Expand Up @@ -126,6 +127,19 @@ export class UserAPI extends AbstractAPI {
});
}

public async updateUserProfileImage(userId: string, params: { file: Blob | File }) {
Comment thread
gkats marked this conversation as resolved.
this.requireId(userId);

const formData = new runtime.FormData();
formData.append('file', params?.file);

return this.request<User>({
method: 'POST',
path: joinPaths(basePath, userId, 'profile_image'),
formData,
});
}

public async updateUserMetadata(userId: string, params: UserMetadataParams) {
this.requireId(userId);

Expand Down
39 changes: 25 additions & 14 deletions packages/backend/src/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type ClerkBackendApiRequestOptions = {
queryParams?: Record<string, unknown>;
headerParams?: Record<string, string>;
bodyParams?: object;
formData?: FormData;
Comment thread
anagstef marked this conversation as resolved.
} & (
| {
url: string;
Expand Down Expand Up @@ -71,7 +72,7 @@ export function buildRequest(options: CreateBackendApiOptions) {
userAgent = USER_AGENT,
httpOptions = {},
} = options;
const { path, method, queryParams, headerParams, bodyParams } = requestOptions;
const { path, method, queryParams, headerParams, bodyParams, formData } = requestOptions;
const key = secretKey || apiKey;

assertValidSecretKey(key);
Expand All @@ -94,30 +95,40 @@ export function buildRequest(options: CreateBackendApiOptions) {
}

// Build headers
const headers = {
const headers: Record<string, any> = {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json',
'Clerk-Backend-SDK': userAgent,
...headerParams,
};

// Build body
const hasBody = method !== 'GET' && bodyParams && Object.keys(bodyParams).length > 0;
const body = hasBody ? { body: JSON.stringify(snakecaseKeys(bodyParams, { deep: false })) } : null;

let res: Response | undefined = undefined;
try {
res = await runtime.fetch(
finalUrl.href,
deepmerge(httpOptions, {
if (formData) {
res = await runtime.fetch(finalUrl.href, {
...httpOptions,
method,
headers,
...body,
}),
);
body: formData,
});
} else {
// Enforce application/json for all non form-data requests
headers['Content-Type'] = 'application/json';
// Build body
const hasBody = method !== 'GET' && bodyParams && Object.keys(bodyParams).length > 0;
const body = hasBody ? { body: JSON.stringify(snakecaseKeys(bodyParams, { deep: false })) } : null;

res = await runtime.fetch(
finalUrl.href,
deepmerge(httpOptions, {
method,
headers,
...body,
}),
);
}

// TODO: Parse JSON or Text response based on a response header
const isJSONResponse = headers && headers['Content-Type'] === 'application/json';
const isJSONResponse = headers['Content-Type'] === 'application/json';
const data = await (isJSONResponse ? res.json() : res.text());

if (!res.ok) {
Expand Down