tududi/backend/modules/oidc/provisioningService.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

191 lines
5.3 KiB
JavaScript

const { User, OIDCIdentity } = require('../../models');
const providerConfig = require('./providerConfig');
const { sequelize } = require('../../models');
function shouldBeAdmin(config, email) {
if (!config.adminEmailDomains || config.adminEmailDomains.length === 0) {
return false;
}
const domain = email.split('@')[1];
return config.adminEmailDomains.includes(domain);
}
function generateUsername(email) {
const baseUsername = email.split('@')[0];
return baseUsername.replace(/[^a-zA-Z0-9_-]/g, '_');
}
async function findOrCreateIdentity(providerSlug, claims) {
const identity = await OIDCIdentity.findOne({
where: {
provider_slug: providerSlug,
subject: claims.sub,
},
include: [{ model: User, as: 'User' }],
});
return identity;
}
async function provisionUser(providerSlug, claims, req) {
const config = providerConfig.getProvider(providerSlug);
if (!config) {
throw new Error(`Provider not found: ${providerSlug}`);
}
const transaction = await sequelize.transaction();
try {
let identity = await OIDCIdentity.findOne({
where: {
provider_slug: providerSlug,
subject: claims.sub,
},
include: [{ model: User, as: 'User' }],
transaction,
});
if (identity) {
await identity.update(
{
last_login_at: new Date(),
email: claims.email || identity.email,
name: claims.name || identity.name,
picture: claims.picture || identity.picture,
raw_claims: claims,
},
{ transaction }
);
await transaction.commit();
return { user: identity.user, isNewUser: false };
}
if (!config.autoProvision) {
await transaction.rollback();
throw new Error('Auto-provisioning is disabled for this provider');
}
if (!claims.email) {
await transaction.rollback();
throw new Error('Email claim is required for provisioning');
}
let user = await User.findOne({
where: { email: claims.email },
transaction,
});
let isNewUser = false;
if (!user) {
const username = generateUsername(claims.email);
user = await User.create(
{
email: claims.email,
username,
verified_email: true,
is_admin: shouldBeAdmin(config, claims.email),
password_digest: null,
},
{ transaction }
);
isNewUser = true;
}
identity = await OIDCIdentity.create(
{
user_id: user.id,
provider_slug: providerSlug,
subject: claims.sub,
email: claims.email,
name: claims.name,
given_name: claims.given_name,
family_name: claims.family_name,
picture: claims.picture,
raw_claims: claims,
first_login_at: new Date(),
last_login_at: new Date(),
},
{ transaction }
);
await transaction.commit();
return { user, isNewUser };
} catch (error) {
await transaction.rollback();
throw error;
}
}
async function linkIdentityToUser(userId, providerSlug, claims) {
const config = providerConfig.getProvider(providerSlug);
if (!config) {
throw new Error(`Provider not found: ${providerSlug}`);
}
const transaction = await sequelize.transaction();
try {
const existingIdentity = await OIDCIdentity.findOne({
where: {
provider_slug: providerSlug,
subject: claims.sub,
},
transaction,
});
if (existingIdentity) {
if (existingIdentity.user_id === userId) {
await transaction.commit();
return existingIdentity;
}
await transaction.rollback();
throw new Error(
'This OIDC identity is already linked to another user'
);
}
const user = await User.findByPk(userId, { transaction });
if (!user) {
await transaction.rollback();
throw new Error('User not found');
}
const identity = await OIDCIdentity.create(
{
user_id: userId,
provider_slug: providerSlug,
subject: claims.sub,
email: claims.email,
name: claims.name,
given_name: claims.given_name,
family_name: claims.family_name,
picture: claims.picture,
raw_claims: claims,
first_login_at: new Date(),
last_login_at: new Date(),
},
{ transaction }
);
await transaction.commit();
return identity;
} catch (error) {
await transaction.rollback();
throw error;
}
}
module.exports = {
provisionUser,
linkIdentityToUser,
findOrCreateIdentity,
shouldBeAdmin,
generateUsername,
};