Merge branch 'harryadel-wekan-accounts-lockout-async-migration'
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions

This commit is contained in:
Lauri Ojansivu 2026-01-30 19:07:41 +02:00
commit 6e9c8a174f
8 changed files with 89 additions and 89 deletions

View file

@ -138,7 +138,7 @@ useraccounts:unstyled@1.14.2
webapp@1.13.8
webapp-hashing@1.1.1
wekan-accounts-cas@0.1.0
wekan-accounts-lockout@1.0.0
wekan-accounts-lockout@1.1.0
wekan-accounts-oidc@1.0.10
wekan-accounts-sandstorm@0.8.0
wekan-fontawesome@6.4.2

View file

@ -59,7 +59,7 @@ if (Meteor.isServer) {
await LockoutSettings._collection.createIndexAsync({ modifiedAt: -1 });
// Known users settings
LockoutSettings.upsert(
await LockoutSettings.upsertAsync(
{ _id: 'known-failuresBeforeLockout' },
{
$setOnInsert: {
@ -71,7 +71,7 @@ if (Meteor.isServer) {
},
);
LockoutSettings.upsert(
await LockoutSettings.upsertAsync(
{ _id: 'known-lockoutPeriod' },
{
$setOnInsert: {
@ -83,7 +83,7 @@ if (Meteor.isServer) {
},
);
LockoutSettings.upsert(
await LockoutSettings.upsertAsync(
{ _id: 'known-failureWindow' },
{
$setOnInsert: {
@ -99,7 +99,7 @@ if (Meteor.isServer) {
const typoVar = process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE;
const correctVar = process.env.ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BEFORE;
LockoutSettings.upsert(
await LockoutSettings.upsertAsync(
{ _id: 'unknown-failuresBeforeLockout' },
{
$setOnInsert: {
@ -111,7 +111,7 @@ if (Meteor.isServer) {
},
);
LockoutSettings.upsert(
await LockoutSettings.upsertAsync(
{ _id: 'unknown-lockoutPeriod' },
{
$setOnInsert: {
@ -123,7 +123,7 @@ if (Meteor.isServer) {
},
);
LockoutSettings.upsert(
await LockoutSettings.upsertAsync(
{ _id: 'unknown-failureWindow' },
{
$setOnInsert: {

View file

@ -2,7 +2,7 @@
Package.describe({
name: 'wekan-accounts-lockout',
version: '1.0.0',
version: '1.1.0',
summary: 'Meteor package for locking user accounts and stopping brute force attacks',
git: 'https://github.com/lucasantoniassi/meteor-accounts-lockout.git',
documentation: 'README.md',

View file

@ -9,12 +9,12 @@ class KnownUser {
this.settings = settings;
}
startup() {
async startup() {
if (!(this.unchangedSettings instanceof Function)) {
this.updateSettings();
}
this.scheduleUnlocksForLockedAccounts();
KnownUser.unlockAccountsIfLockoutAlreadyExpired();
await this.scheduleUnlocksForLockedAccounts();
await KnownUser.unlockAccountsIfLockoutAlreadyExpired();
this.hookIntoAccounts();
}
@ -49,7 +49,7 @@ class KnownUser {
}
}
scheduleUnlocksForLockedAccounts() {
async scheduleUnlocksForLockedAccounts() {
const lockedAccountsCursor = Meteor.users.find(
{
'services.accounts-lockout.unlockTime': {
@ -63,7 +63,7 @@ class KnownUser {
},
);
const currentTime = Number(new Date());
lockedAccountsCursor.forEach((user) => {
for await (const user of lockedAccountsCursor) {
let lockDuration = KnownUser.unlockTime(user) - currentTime;
if (lockDuration >= this.settings.lockoutPeriod) {
lockDuration = this.settings.lockoutPeriod * 1000;
@ -75,10 +75,10 @@ class KnownUser {
KnownUser.unlockAccount.bind(null, user._id),
lockDuration,
);
});
}
}
static unlockAccountsIfLockoutAlreadyExpired() {
static async unlockAccountsIfLockoutAlreadyExpired() {
const currentTime = Number(new Date());
const query = {
'services.accounts-lockout.unlockTime': {
@ -91,7 +91,7 @@ class KnownUser {
'services.accounts-lockout.failedAttempts': 0,
},
};
Meteor.users.update(query, data);
await Meteor.users.updateAsync(query, data);
}
hookIntoAccounts() {
@ -100,7 +100,7 @@ class KnownUser {
}
validateLoginAttempt(loginInfo) {
async validateLoginAttempt(loginInfo) {
if (
// don't interrupt non-password logins
loginInfo.type !== 'password' ||
@ -130,12 +130,12 @@ class KnownUser {
const canReset = (currentTime - firstFailedAttempt) > (1000 * this.settings.failureWindow);
if (canReset) {
failedAttempts = 1;
KnownUser.resetAttempts(failedAttempts, userId);
await KnownUser.resetAttempts(failedAttempts, userId);
}
const canIncrement = failedAttempts < this.settings.failuresBeforeLockout;
if (canIncrement) {
KnownUser.incrementAttempts(failedAttempts, userId);
await KnownUser.incrementAttempts(failedAttempts, userId);
}
const maxAttemptsAllowed = this.settings.failuresBeforeLockout;
@ -147,7 +147,7 @@ class KnownUser {
KnownUser.tooManyAttempts(duration);
}
if (failedAttempts === maxAttemptsAllowed) {
this.setNewUnlockTime(failedAttempts, userId);
await this.setNewUnlockTime(failedAttempts, userId);
let duration = this.settings.lockoutPeriod;
duration = Math.ceil(duration);
@ -161,7 +161,7 @@ class KnownUser {
);
}
static resetAttempts(
static async resetAttempts(
failedAttempts,
userId,
) {
@ -174,10 +174,10 @@ class KnownUser {
'services.accounts-lockout.firstFailedAttempt': currentTime,
},
};
Meteor.users.update(query, data);
await Meteor.users.updateAsync(query, data);
}
static incrementAttempts(
static async incrementAttempts(
failedAttempts,
userId,
) {
@ -189,10 +189,10 @@ class KnownUser {
'services.accounts-lockout.lastFailedAttempt': currentTime,
},
};
Meteor.users.update(query, data);
await Meteor.users.updateAsync(query, data);
}
setNewUnlockTime(
async setNewUnlockTime(
failedAttempts,
userId,
) {
@ -206,14 +206,14 @@ class KnownUser {
'services.accounts-lockout.unlockTime': newUnlockTime,
},
};
Meteor.users.update(query, data);
await Meteor.users.updateAsync(query, data);
Meteor.setTimeout(
KnownUser.unlockAccount.bind(null, userId),
this.settings.lockoutPeriod * 1000,
);
}
static onLogin(loginInfo) {
static async onLogin(loginInfo) {
if (loginInfo.type !== 'password') {
return;
}
@ -225,7 +225,7 @@ class KnownUser {
'services.accounts-lockout.failedAttempts': 0,
},
};
Meteor.users.update(query, data);
await Meteor.users.updateAsync(query, data);
}
static incorrectPassword(
@ -306,7 +306,7 @@ class KnownUser {
return firstFailedAttempt || 0;
}
static unlockAccount(userId) {
static async unlockAccount(userId) {
const query = { _id: userId };
const data = {
$unset: {
@ -314,7 +314,7 @@ class KnownUser {
'services.accounts-lockout.failedAttempts': 0,
},
};
Meteor.users.update(query, data);
await Meteor.users.updateAsync(query, data);
}
}

View file

@ -13,12 +13,12 @@ class UnknownUser {
this.settings = settings;
}
startup() {
async startup() {
if (!(this.settings instanceof Function)) {
this.updateSettings();
}
this.scheduleUnlocksForLockedAccounts();
this.unlockAccountsIfLockoutAlreadyExpired();
await this.scheduleUnlocksForLockedAccounts();
await this.unlockAccountsIfLockoutAlreadyExpired();
this.hookIntoAccounts();
}
@ -53,7 +53,7 @@ class UnknownUser {
}
}
scheduleUnlocksForLockedAccounts() {
async scheduleUnlocksForLockedAccounts() {
const lockedAccountsCursor = this.AccountsLockoutCollection.find(
{
'services.accounts-lockout.unlockTime': {
@ -67,8 +67,8 @@ class UnknownUser {
},
);
const currentTime = Number(new Date());
lockedAccountsCursor.forEach((connection) => {
let lockDuration = this.unlockTime(connection) - currentTime;
for await (const connection of lockedAccountsCursor) {
let lockDuration = await this.unlockTime(connection) - currentTime;
if (lockDuration >= this.settings.lockoutPeriod) {
lockDuration = this.settings.lockoutPeriod * 1000;
}
@ -79,10 +79,10 @@ class UnknownUser {
this.unlockAccount.bind(this, connection.clientAddress),
lockDuration,
);
});
}
}
unlockAccountsIfLockoutAlreadyExpired() {
async unlockAccountsIfLockoutAlreadyExpired() {
const currentTime = Number(new Date());
const query = {
'services.accounts-lockout.unlockTime': {
@ -95,7 +95,7 @@ class UnknownUser {
'services.accounts-lockout.failedAttempts': 0,
},
};
this.AccountsLockoutCollection.update(query, data);
await this.AccountsLockoutCollection.updateAsync(query, data);
}
hookIntoAccounts() {
@ -103,7 +103,7 @@ class UnknownUser {
Accounts.onLogin(this.onLogin.bind(this));
}
validateLoginAttempt(loginInfo) {
async validateLoginAttempt(loginInfo) {
// don't interrupt non-password logins
if (
loginInfo.type !== 'password' ||
@ -120,20 +120,20 @@ class UnknownUser {
}
const clientAddress = loginInfo.connection.clientAddress;
const unlockTime = this.unlockTime(loginInfo.connection);
let failedAttempts = 1 + this.failedAttempts(loginInfo.connection);
const firstFailedAttempt = this.firstFailedAttempt(loginInfo.connection);
const unlockTime = await this.unlockTime(loginInfo.connection);
let failedAttempts = 1 + await this.failedAttempts(loginInfo.connection);
const firstFailedAttempt = await this.firstFailedAttempt(loginInfo.connection);
const currentTime = Number(new Date());
const canReset = (currentTime - firstFailedAttempt) > (1000 * this.settings.failureWindow);
if (canReset) {
failedAttempts = 1;
this.resetAttempts(failedAttempts, clientAddress);
await this.resetAttempts(failedAttempts, clientAddress);
}
const canIncrement = failedAttempts < this.settings.failuresBeforeLockout;
if (canIncrement) {
this.incrementAttempts(failedAttempts, clientAddress);
await this.incrementAttempts(failedAttempts, clientAddress);
}
const maxAttemptsAllowed = this.settings.failuresBeforeLockout;
@ -145,7 +145,7 @@ class UnknownUser {
UnknownUser.tooManyAttempts(duration);
}
if (failedAttempts === maxAttemptsAllowed) {
this.setNewUnlockTime(failedAttempts, clientAddress);
await this.setNewUnlockTime(failedAttempts, clientAddress);
let duration = this.settings.lockoutPeriod;
duration = Math.ceil(duration);
@ -159,7 +159,7 @@ class UnknownUser {
);
}
resetAttempts(
async resetAttempts(
failedAttempts,
clientAddress,
) {
@ -172,10 +172,10 @@ class UnknownUser {
'services.accounts-lockout.firstFailedAttempt': currentTime,
},
};
this.AccountsLockoutCollection.upsert(query, data);
await this.AccountsLockoutCollection.upsertAsync(query, data);
}
incrementAttempts(
async incrementAttempts(
failedAttempts,
clientAddress,
) {
@ -187,10 +187,10 @@ class UnknownUser {
'services.accounts-lockout.lastFailedAttempt': currentTime,
},
};
this.AccountsLockoutCollection.upsert(query, data);
await this.AccountsLockoutCollection.upsertAsync(query, data);
}
setNewUnlockTime(
async setNewUnlockTime(
failedAttempts,
clientAddress,
) {
@ -204,14 +204,14 @@ class UnknownUser {
'services.accounts-lockout.unlockTime': newUnlockTime,
},
};
this.AccountsLockoutCollection.upsert(query, data);
await this.AccountsLockoutCollection.upsertAsync(query, data);
Meteor.setTimeout(
this.unlockAccount.bind(this, clientAddress),
this.settings.lockoutPeriod * 1000,
);
}
onLogin(loginInfo) {
async onLogin(loginInfo) {
if (loginInfo.type !== 'password') {
return;
}
@ -223,7 +223,7 @@ class UnknownUser {
'services.accounts-lockout.failedAttempts': 0,
},
};
this.AccountsLockoutCollection.update(query, data);
await this.AccountsLockoutCollection.updateAsync(query, data);
}
static userNotFound(
@ -264,14 +264,14 @@ class UnknownUser {
return unknownUsers || false;
}
findOneByConnection(connection) {
return this.AccountsLockoutCollection.findOne({
async findOneByConnection(connection) {
return await this.AccountsLockoutCollection.findOneAsync({
clientAddress: connection.clientAddress,
});
}
unlockTime(connection) {
connection = this.findOneByConnection(connection);
async unlockTime(connection) {
connection = await this.findOneByConnection(connection);
let unlockTime;
try {
unlockTime = connection.services['accounts-lockout'].unlockTime;
@ -281,8 +281,8 @@ class UnknownUser {
return unlockTime || 0;
}
failedAttempts(connection) {
connection = this.findOneByConnection(connection);
async failedAttempts(connection) {
connection = await this.findOneByConnection(connection);
let failedAttempts;
try {
failedAttempts = connection.services['accounts-lockout'].failedAttempts;
@ -292,8 +292,8 @@ class UnknownUser {
return failedAttempts || 0;
}
lastFailedAttempt(connection) {
connection = this.findOneByConnection(connection);
async lastFailedAttempt(connection) {
connection = await this.findOneByConnection(connection);
let lastFailedAttempt;
try {
lastFailedAttempt = connection.services['accounts-lockout'].lastFailedAttempt;
@ -303,8 +303,8 @@ class UnknownUser {
return lastFailedAttempt || 0;
}
firstFailedAttempt(connection) {
connection = this.findOneByConnection(connection);
async firstFailedAttempt(connection) {
connection = await this.findOneByConnection(connection);
let firstFailedAttempt;
try {
firstFailedAttempt = connection.services['accounts-lockout'].firstFailedAttempt;
@ -314,7 +314,7 @@ class UnknownUser {
return firstFailedAttempt || 0;
}
unlockAccount(clientAddress) {
async unlockAccount(clientAddress) {
const query = { clientAddress };
const data = {
$unset: {
@ -322,7 +322,7 @@ class UnknownUser {
'services.accounts-lockout.failedAttempts': 0,
},
};
this.AccountsLockoutCollection.update(query, data);
await this.AccountsLockoutCollection.updateAsync(query, data);
}
}

View file

@ -1,21 +1,21 @@
import { AccountsLockout } from 'meteor/wekan-accounts-lockout';
import LockoutSettings from '/models/lockoutSettings';
Meteor.startup(() => {
Meteor.startup(async () => {
// Wait for the database to be ready
Meteor.setTimeout(() => {
Meteor.setTimeout(async () => {
try {
// Get configurations from database
const knownUsersConfig = {
failuresBeforeLockout: LockoutSettings.findOne('known-failuresBeforeLockout')?.value || 3,
lockoutPeriod: LockoutSettings.findOne('known-lockoutPeriod')?.value || 60,
failureWindow: LockoutSettings.findOne('known-failureWindow')?.value || 15
failuresBeforeLockout: (await LockoutSettings.findOneAsync('known-failuresBeforeLockout'))?.value || 3,
lockoutPeriod: (await LockoutSettings.findOneAsync('known-lockoutPeriod'))?.value || 60,
failureWindow: (await LockoutSettings.findOneAsync('known-failureWindow'))?.value || 15
};
const unknownUsersConfig = {
failuresBeforeLockout: LockoutSettings.findOne('unknown-failuresBeforeLockout')?.value || 3,
lockoutPeriod: LockoutSettings.findOne('unknown-lockoutPeriod')?.value || 60,
failureWindow: LockoutSettings.findOne('unknown-failureWindow')?.value || 15
failuresBeforeLockout: (await LockoutSettings.findOneAsync('unknown-failuresBeforeLockout'))?.value || 3,
lockoutPeriod: (await LockoutSettings.findOneAsync('unknown-lockoutPeriod'))?.value || 60,
failureWindow: (await LockoutSettings.findOneAsync('unknown-failureWindow'))?.value || 15
};
// Initialize the AccountsLockout with configuration

View file

@ -2,7 +2,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
// Method to find locked users and release them if needed
Meteor.methods({
getLockedUsers() {
async getLockedUsers() {
// Check if user has admin rights
const userId = Meteor.userId();
if (!userId) {
@ -17,7 +17,7 @@ Meteor.methods({
const currentTime = Number(new Date());
// Find users that are locked (known users)
const lockedUsers = Meteor.users.find(
const lockedUsers = await Meteor.users.find(
{
'services.accounts-lockout.unlockTime': {
$gt: currentTime,
@ -32,7 +32,7 @@ Meteor.methods({
'services.accounts-lockout.failedAttempts': 1
}
}
).fetch();
).fetchAsync();
// Format the results for the UI
return lockedUsers.map(user => {
@ -50,7 +50,7 @@ Meteor.methods({
});
},
unlockUser(userId) {
async unlockUser(userId) {
// Check if user has admin rights
const adminId = Meteor.userId();
if (!adminId) {
@ -62,13 +62,13 @@ Meteor.methods({
}
// Make sure the user to unlock exists
const userToUnlock = Meteor.users.findOne(userId);
const userToUnlock = await Meteor.users.findOneAsync(userId);
if (!userToUnlock) {
throw new Meteor.Error('error-user-not-found', 'User not found');
}
// Unlock the user
Meteor.users.update(
await Meteor.users.updateAsync(
{ _id: userId },
{
$unset: {
@ -80,7 +80,7 @@ Meteor.methods({
return true;
},
unlockAllUsers() {
async unlockAllUsers() {
// Check if user has admin rights
const adminId = Meteor.userId();
if (!adminId) {
@ -92,7 +92,7 @@ Meteor.methods({
}
// Unlock all users
Meteor.users.update(
await Meteor.users.updateAsync(
{ 'services.accounts-lockout.unlockTime': { $exists: true } },
{
$unset: {

View file

@ -3,7 +3,7 @@ import { ReactiveCache } from '/imports/reactiveCache';
import LockoutSettings from '/models/lockoutSettings';
Meteor.methods({
reloadAccountsLockout() {
async reloadAccountsLockout() {
// Check if user has admin rights
const userId = Meteor.userId();
if (!userId) {
@ -17,15 +17,15 @@ Meteor.methods({
try {
// Get configurations from database
const knownUsersConfig = {
failuresBeforeLockout: LockoutSettings.findOne('known-failuresBeforeLockout')?.value || 3,
lockoutPeriod: LockoutSettings.findOne('known-lockoutPeriod')?.value || 60,
failureWindow: LockoutSettings.findOne('known-failureWindow')?.value || 15
failuresBeforeLockout: (await LockoutSettings.findOneAsync('known-failuresBeforeLockout'))?.value || 3,
lockoutPeriod: (await LockoutSettings.findOneAsync('known-lockoutPeriod'))?.value || 60,
failureWindow: (await LockoutSettings.findOneAsync('known-failureWindow'))?.value || 15
};
const unknownUsersConfig = {
failuresBeforeLockout: LockoutSettings.findOne('unknown-failuresBeforeLockout')?.value || 3,
lockoutPeriod: LockoutSettings.findOne('unknown-lockoutPeriod')?.value || 60,
failureWindow: LockoutSettings.findOne('unknown-failureWindow')?.value || 15
failuresBeforeLockout: (await LockoutSettings.findOneAsync('unknown-failuresBeforeLockout'))?.value || 3,
lockoutPeriod: (await LockoutSettings.findOneAsync('unknown-lockoutPeriod'))?.value || 60,
failureWindow: (await LockoutSettings.findOneAsync('unknown-failureWindow'))?.value || 15
};
// Initialize the AccountsLockout with configuration