feat: add azure blob storage upload#34
Conversation
lorenzocorallo
commented
Jun 2, 2026
- add azure blob storage client
- add endpoint to change user profile picture
WalkthroughThis PR adds Azure Blob Storage integration to enable user profile picture uploads. The version is bumped to 0.16.0 with the blob SDK dependency added. A new ChangesProfile Picture Upload via Azure Blob Storage
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/azure/blob.ts`:
- Line 22: The upload call currently uses blobClient.upload(buffer,
buffer.length) which stores blobs as application/octet-stream; update the upload
invocation to pass the original MIME type via the upload options by supplying
blobHTTPHeaders.blobContentType (e.g., use blobClient.upload(buffer,
buffer.length, { blobHTTPHeaders: { blobContentType: contentType } })),
threading the original contentType/mimeType value from wherever the file/origin
is provided into the call so images render in browsers.
- Line 19: The filename generation using `const filename =
\`upload_${Date.now()}.${extension}\`` can collide under concurrent uploads;
modify the logic that builds `filename` to append or replace the timestamp with
a strong random component from the existing `nanoid` dependency (e.g., combine
`Date.now()` and `nanoid()` or use `nanoid()` alone) to guarantee uniqueness and
unpredictability, ensure `nanoid` is imported where `filename` is created
(import { nanoid } from 'nanoid'), and keep the file extension from `extension`
while producing a blob-safe string.
In `@src/routers/auth.ts`:
- Around line 9-29: The updateProfilePic handler currently defined as
publicProcedure trusts input.userId and updates SCHEMA.AUTH.user, enabling IDOR;
change it to use the authenticated identity (ctx.userId) instead of the input
userId or make it a protected/authenticated procedure and assert input.userId
=== ctx.userId before performing the DB.update in updateProfilePic; ensure you
reference the trpc context user id (ctx.userId from server.ts) and only call
DB.update(SCHEMA.AUTH.user).set(...) when the authenticated id matches the
target.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 43b933e9-bba2-4dbe-b64e-e0568d0b0730
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
package.jsonpackage/package.jsonsrc/azure/blob.tssrc/env.tssrc/routers/auth.tssrc/routers/index.tssrc/server.ts
| export const storageClient = new BlobServiceClient( | ||
| `https://${storageAccount}.blob.core.windows.net`, | ||
| defaultAzureCredential | ||
| ) | ||
|
|
||
| const containerName = env.AZURE_BLOB_STORAGE_CONTAINER | ||
| export const containerClient = storageClient.getContainerClient(containerName) | ||
|
|
||
| export async function uploadBlob(buffer: Buffer, extension: string) { | ||
| const filename = `upload_${Date.now()}.${extension}` | ||
| const blobClient = containerClient.getBlockBlobClient(filename) | ||
|
|
||
| const res = await blobClient.upload(buffer, buffer.length) | ||
| logger.info(`Upload block blob ${filename} successfully with request ID: ${res.requestId}`) | ||
| return { filename, url: `https://${storageAccount}.blob.core.windows.net/${containerName}/${filename}` } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Azure Blob Storage container public access disabled by default 2024 anonymous blob access
💡 Result:
Yes, Azure Blob Storage accounts and individual containers are configured to disable anonymous public access by default [1][2][3]. As of the most recent updates, Microsoft has enforced a security posture where anonymous access is restricted at both the storage account level and the container level out of the box [2][3]. Key details regarding this configuration include: 1. Storage Account Level: New Azure Storage accounts are created with the AllowBlobPublicAccess property set to False by default [3]. This setting acts as a master override; even if a specific container is configured to allow anonymous access, the storage account level setting prevents any anonymous requests from succeeding [2][4]. 2. Container Level: By default, individual containers are also set to private, meaning no anonymous access is permitted unless specifically authorized by a user with sufficient permissions [1][5]. 3. Enabling Access: To permit anonymous access, an administrator must first explicitly enable anonymous access for the entire storage account and subsequently configure the desired anonymous access level on individual containers [1][4]. 4. Security Best Practices: Because anonymous access presents a security risk, it is strongly recommended to use secure authorization methods such as Microsoft Entra ID (formerly Azure AD), Shared Access Signatures (SAS), or account keys instead of enabling public access [6][7][8]. Existing storage accounts created before these policy updates were rolled out may retain their previous configurations, but for all new deployments, the secure default is enforced [3]. Use Azure Policy to consistently enforce the restriction of anonymous access across your organization's storage environments [9].
Citations:
- 1: https://learn.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-configure
- 2: https://learn.microsoft.com/en-us/azure/storage/blobs/anonymous-read-access-prevent
- 3: https://techcommunity.microsoft.com/blog/azurestorageblog/azure-storage-updating-some-default-security-settings-on-new-accounts---rollout-/3819554
- 4: https://learn.microsoft.com/en-us/answers/questions/1791616/whats-the-use-of-anonymous-access-at-storage-accou
- 5: https://github.com/MicrosoftDocs/azure-docs/blob/main/articles/storage/blobs/anonymous-read-access-prevent.md
- 6: https://dev.to/chinua_ifeanyi_fe2c942ff1/implementing-secure-file-access-in-azure-blob-storage-using-shared-access-signatures-sas-59e2
- 7: https://dev.to/wokoma/azure-blob-storage-for-beginners-private-access-sas-tokens-cost-savings-explained-43ma
- 8: https://learn.microsoft.com/en-us/answers/questions/2032538/enabled-anonymous-access-to-contain-via-browser
- 9: https://www.reddit.com/r/AZURE/comments/1akdb32/azure_policy_allow_blob_anonymous_access/
Fix blob URL accessibility (may 403 without SAS/auth)
uploadBlob returns a bare https://{account}.blob.core.windows.net/{container}/{filename} URL with no SAS token. Since Azure Storage anonymous/public blob access is disabled by default at the storage account/container level, unauthenticated clients will typically get 403/AuthenticationRequired unless anonymous read is explicitly enabled.
- If containers are private, return a SAS URL (or otherwise provide an authenticated access path) instead of a “public” URL.
- At minimum, ensure the container’s access level matches the assumption that this URL is publicly reachable.
| export const containerClient = storageClient.getContainerClient(containerName) | ||
|
|
||
| export async function uploadBlob(buffer: Buffer, extension: string) { | ||
| const filename = `upload_${Date.now()}.${extension}` |
There was a problem hiding this comment.
Timestamp-only filenames can collide and overwrite blobs.
upload_${Date.now()}.${extension} is not unique under concurrent uploads (same millisecond) and is predictable. nanoid is already a dependency — add a random component to avoid clobbering.
🔧 Proposed fix
-import { DefaultAzureCredential } from "`@azure/identity`"
-import { BlobServiceClient } from "`@azure/storage-blob`"
-import { env } from "`@/env`"
-import { logger } from "`@/logger`"
+import { DefaultAzureCredential } from "`@azure/identity`"
+import { BlobServiceClient } from "`@azure/storage-blob`"
+import { nanoid } from "nanoid"
+import { env } from "`@/env`"
+import { logger } from "`@/logger`"- const filename = `upload_${Date.now()}.${extension}`
+ const filename = `upload_${Date.now()}_${nanoid()}.${extension}`🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/azure/blob.ts` at line 19, The filename generation using `const filename
= \`upload_${Date.now()}.${extension}\`` can collide under concurrent uploads;
modify the logic that builds `filename` to append or replace the timestamp with
a strong random component from the existing `nanoid` dependency (e.g., combine
`Date.now()` and `nanoid()` or use `nanoid()` alone) to guarantee uniqueness and
unpredictability, ensure `nanoid` is imported where `filename` is created
(import { nanoid } from 'nanoid'), and keep the file extension from `extension`
while producing a blob-safe string.
| const filename = `upload_${Date.now()}.${extension}` | ||
| const blobClient = containerClient.getBlockBlobClient(filename) | ||
|
|
||
| const res = await blobClient.upload(buffer, buffer.length) |
There was a problem hiding this comment.
Set the blob content type on upload.
upload(buffer, buffer.length) stores the blob without blobContentType, so it defaults to application/octet-stream. Browsers will download rather than render the image, and the served URL won't behave as an image. Pass the original MIME type through.
🔧 Proposed fix (thread contentType through)
-export async function uploadBlob(buffer: Buffer, extension: string) {
+export async function uploadBlob(buffer: Buffer, extension: string, contentType: string) {
const filename = `upload_${Date.now()}.${extension}`
const blobClient = containerClient.getBlockBlobClient(filename)
- const res = await blobClient.upload(buffer, buffer.length)
+ const res = await blobClient.upload(buffer, buffer.length, {
+ blobHTTPHeaders: { blobContentType: contentType },
+ })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/azure/blob.ts` at line 22, The upload call currently uses
blobClient.upload(buffer, buffer.length) which stores blobs as
application/octet-stream; update the upload invocation to pass the original MIME
type via the upload options by supplying blobHTTPHeaders.blobContentType (e.g.,
use blobClient.upload(buffer, buffer.length, { blobHTTPHeaders: {
blobContentType: contentType } })), threading the original contentType/mimeType
value from wherever the file/origin is provided into the call so images render
in browsers.
| updateProfilePic: publicProcedure | ||
| .input( | ||
| z | ||
| .instanceof(FormData) | ||
| .transform((fd): Record<string, string | File> => Object.fromEntries(fd.entries())) | ||
| .pipe( | ||
| z.object({ | ||
| userId: z.string(), | ||
| image: z | ||
| .file() | ||
| .mime(["image/png", "image/jpeg"]) | ||
| .min(1) | ||
| .max(1024 * 1024), // 1MB | ||
| }) | ||
| ) | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| try { | ||
| const buffer = Buffer.from(await input.image.arrayBuffer()) | ||
| const file = await uploadBlob(buffer, input.image.type.includes("png") ? "png" : "jpeg") | ||
| await DB.update(SCHEMA.AUTH.user).set({ image: file.url }).where(eq(SCHEMA.AUTH.user.id, input.userId)) |
There was a problem hiding this comment.
Broken access control: any unauthenticated caller can change any user's profile picture.
updateProfilePic is a publicProcedure that takes userId directly from input and updates SCHEMA.AUTH.user for that id. There is no authentication or authorization check, so anyone can overwrite an arbitrary user's image (IDOR). The TRPC context already exposes an identity (ctx.userId from the request header in server.ts), but it's ignored here.
Use a protected/authenticated procedure and derive the target from the authenticated session rather than trusting input.userId (or, at minimum, assert input.userId === ctx.userId).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/routers/auth.ts` around lines 9 - 29, The updateProfilePic handler
currently defined as publicProcedure trusts input.userId and updates
SCHEMA.AUTH.user, enabling IDOR; change it to use the authenticated identity
(ctx.userId) instead of the input userId or make it a protected/authenticated
procedure and assert input.userId === ctx.userId before performing the DB.update
in updateProfilePic; ensure you reference the trpc context user id (ctx.userId
from server.ts) and only call DB.update(SCHEMA.AUTH.user).set(...) when the
authenticated id matches the target.