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
2 changes: 1 addition & 1 deletion public/users/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ <h3>Link another account</h3>
${c.provider.charAt(0).toUpperCase() + c.provider.slice(1)}
${isCurrent ? '<span class="current-badge">logged in</span>' : ''}
</div>
<div style="font-size: 0.8rem; color: #666;">${c.profile_data.email || c.subject_id}</div>
<div style="font-size: 0.8rem; color: #666;">${c.email || c.subject_id}</div>
</div>
</div>
<button class="remove-btn" onclick="removeCredential('${c.provider}')" ${isCurrent || credentials.length === 1 ? 'disabled title="' + (isCurrent ? 'Cannot remove the method you are currently logged in with' : 'Cannot remove your last login method') + '"' : ''}>
Expand Down
9 changes: 8 additions & 1 deletion src/UserDO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,14 @@ export class UserDO extends DurableObject {
for (const row of credentialsMapping) {
const stub = this.env.CREDENTIAL.get(this.env.CREDENTIAL.idFromName(row.provider as string));
const providerCreds = await stub.list(this.ctx.id.toString());
credentials.push(...providerCreds.map((c: any) => ({ provider: row.provider, ...c })));
credentials.push(
...providerCreds.map((c: any) => ({
provider: row.provider,
subject_id: c.subject_id,
email: c.profile_data?.email,
created_at: c.created_at,
})),
);
}
return credentials;
}
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,7 +1055,7 @@ function renderCredentialsList(credentials: any[], currentProvider?: string): st
${c.provider.charAt(0).toUpperCase() + c.provider.slice(1)}
${isCurrent ? '<span class="current-badge">logged in</span>' : ''}
</div>
<div style="font-size: 0.8rem; color: #666;">${c.profile_data?.email || c.subject_id}</div>
<div style="font-size: 0.8rem; color: #666;">${c.email || c.subject_id}</div>
</div>
</div>
<button class="remove-btn" onclick="removeCredential('${c.provider}')" ${isCurrent || credentials.length === 1 ? 'disabled title="' + (isCurrent ? 'Cannot remove the method you are currently logged in with' : 'Cannot remove your last login method') + '"' : ''}>
Expand Down
9 changes: 8 additions & 1 deletion src/schemas/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ export const CredentialSchema = z.object({
subject_id: z.string(),
});

export type Credential = z.infer<typeof CredentialSchema>;
export const PublicCredentialSchema = z.object({
provider: z.string(),
subject_id: z.string(),
email: z.string().optional(),
created_at: z.number().optional(),
});

export type PublicCredential = z.infer<typeof PublicCredentialSchema>;

export const OAuthCredentialSchema = z.object({
subject_id: z.string(),
Expand Down
31 changes: 31 additions & 0 deletions test/userdo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,35 @@ describe('UserDO Durable Object', () => {
expect(memberships[0].role).toBe(role);
expect(memberships[0].is_current).toBe(1);
});

it('should list credentials without exposing sensitive data', async () => {
const id = env.USER.newUniqueId();
const stub = env.USER.get(id);

// Mock CredentialDO behavior
const googleCredStub = env.CREDENTIAL.get(env.CREDENTIAL.idFromName('google'));
await googleCredStub.put({
user_id: id.toString(),
subject_id: 'g123',
access_token: 'secret-token',
refresh_token: 'secret-refresh',
profile_data: { email: 'user@example.com', extra: 'sensitive' },
});

await stub.addCredential('google', 'g123');

const credentials = await stub.listCredentials();
expect(credentials).toHaveLength(1);
expect(credentials[0]).toEqual({
provider: 'google',
subject_id: 'g123',
email: 'user@example.com',
created_at: expect.any(Number),
});

// Ensure sensitive fields are NOT present
expect(credentials[0]).not.toHaveProperty('access_token');
expect(credentials[0]).not.toHaveProperty('refresh_token');
expect(credentials[0]).not.toHaveProperty('profile_data');
});
});