tududi/backend/modules/oidc/oidcIdentityService.js
Chris 34dc0373fb
fix: correct Sequelize alias case for OIDCIdentity-User association (#1015)
* fix: use nullish coalescing for recurrence weekday to allow Sunday selection

Fixes #812

When creating a "Monthly on weekday" recurring task, the selector would
jump back to Monday when trying to select Sunday. This was caused by using
the logical OR operator (||) instead of the nullish coalescing operator (??)
when handling the recurrence_weekday value.

Since Sunday is represented as 0, the || operator treated it as falsy and
defaulted to null/undefined, which then defaulted to 1 (Monday).

Changes:
- Replace || with ?? for recurrence_weekday in TaskRecurrenceCard.tsx
- Replace || with ?? for recurrence_weekday in TaskDetails.tsx
- Also fix recurrence_week_of_month and recurrence_month_day for consistency

* fix: correct Sequelize alias case for OIDCIdentity-User association

Fixes #1013

Changed all instances of lowercase 'user' to 'User' to match the
association defined in models/index.js. This resolves the Sequelize
error during OIDC callback:
"User is associated to OIDCIdentity using an alias. You've included
an alias (user), but it does not match the alias(es) defined in your
association (User)."

Changes:
- oidcIdentityService.js: 'user' -> 'User'
- provisioningService.js: 'user' -> 'User' (2 instances)
2026-04-13 19:29:50 +03:00

124 lines
3 KiB
JavaScript

const { OIDCIdentity, User } = require('../../models');
async function getUserIdentities(userId) {
return await OIDCIdentity.findAll({
where: { user_id: userId },
order: [['created_at', 'DESC']],
attributes: [
'id',
'provider_slug',
'email',
'name',
'picture',
'first_login_at',
'last_login_at',
'created_at',
],
});
}
async function getIdentityById(identityId) {
return await OIDCIdentity.findByPk(identityId, {
include: [
{
model: User,
as: 'User',
attributes: ['id', 'email', 'username', 'is_admin'],
},
],
});
}
async function unlinkIdentity(identityId, userId) {
const identity = await OIDCIdentity.findOne({
where: {
id: identityId,
user_id: userId,
},
});
if (!identity) {
throw new Error('Identity not found or does not belong to this user');
}
const user = await User.findByPk(userId);
const hasPassword = !!user.password_digest;
const otherIdentities = await OIDCIdentity.count({
where: {
user_id: userId,
id: { [require('sequelize').Op.ne]: identityId },
},
});
if (!hasPassword && otherIdentities === 0) {
throw new Error(
'Cannot unlink the last authentication method. Please set a password first or link another provider.'
);
}
await identity.destroy();
return true;
}
async function canUnlink(identityId, userId) {
const identity = await OIDCIdentity.findOne({
where: {
id: identityId,
user_id: userId,
},
});
if (!identity) {
return { canUnlink: false, reason: 'Identity not found' };
}
const user = await User.findByPk(userId);
const hasPassword = !!user.password_digest;
const otherIdentities = await OIDCIdentity.count({
where: {
user_id: userId,
id: { [require('sequelize').Op.ne]: identityId },
},
});
if (!hasPassword && otherIdentities === 0) {
return {
canUnlink: false,
reason: 'This is your only authentication method',
};
}
return { canUnlink: true };
}
async function updateIdentityClaims(identityId, claims) {
const identity = await OIDCIdentity.findByPk(identityId);
if (!identity) {
throw new Error('Identity not found');
}
await identity.update({
email: claims.email || identity.email,
name: claims.name || identity.name,
given_name: claims.given_name || identity.given_name,
family_name: claims.family_name || identity.family_name,
picture: claims.picture || identity.picture,
raw_claims: claims,
last_login_at: new Date(),
});
return identity;
}
module.exports = {
getUserIdentities,
getIdentityById,
unlinkIdentity,
canUnlink,
updateIdentityClaims,
};