* 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)
191 lines
5.3 KiB
JavaScript
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,
|
|
};
|