From f840b07c02ea64d330ec58d933f5b3bdb49efffb Mon Sep 17 00:00:00 2001 From: Firas Saidi Date: Mon, 21 Apr 2025 20:50:23 +0100 Subject: [PATCH] documentation/login-and-signup --- READMELoginSignUp.md | 113 ++++++++++++++++++ models/activities.js | 267 +++++++++++++++++++++++++++++-------------- 2 files changed, 297 insertions(+), 83 deletions(-) create mode 100644 READMELoginSignUp.md diff --git a/READMELoginSignUp.md b/READMELoginSignUp.md new file mode 100644 index 000000000..9ce73a3aa --- /dev/null +++ b/READMELoginSignUp.md @@ -0,0 +1,113 @@ +# πŸ” WeKan β€” Login System Overview + +This document provides a detailed overview of WeKan’s **login and authentication system**, covering client-side UI, server-side logic, external authentication methods, and potential upgrade paths. + +--- + +## πŸ–₯️ Login Web UI + +WeKan's login interface is implemented using a combination of: + +- `layouts.jade` – Login HTML structure +- `layouts.js` – Login logic and interactivity +- `layouts.css` – Styling and layout + +πŸ“ Source: [`client/components/main`](https://github.com/wekan/wekan/tree/main/client/components/main) + +--- + +## βš™οΈ Server-Side Authentication + +Server-side login functionality is handled in: + +- [`server/authentication.js`](https://github.com/wekan/wekan/blob/main/server/authentication.js) + +Other related configurations: + +- πŸ”§ Account config: [`config/accounts.js`](https://github.com/wekan/wekan/blob/main/config/accounts.js) +- πŸ“¨ Sign-up invitations: [`models/settings.js#L275`](https://github.com/wekan/wekan/blob/main/models/settings.js#L275) +- πŸ‘€ User creation logic: [`models/users.js#L1339`](https://github.com/wekan/wekan/blob/main/models/users.js#L1339) + +--- + +## πŸ‘₯ Meteor User Accounts + +WeKan utilizes Meteor’s `accounts` system. Relevant resources: + +- πŸ“š Meteor 2.x Accounts Docs: [v2-docs.meteor.com/api/accounts](https://v2-docs.meteor.com/api/accounts) +- πŸ” Meteor Packages: + - [`packages`](https://github.com/wekan/wekan/blob/main/.meteor/packages) + - [`versions`](https://github.com/wekan/wekan/blob/main/.meteor/versions) +- πŸ“¦ Meteor 2.14 core packages: [Meteor 2.14 packages](https://github.com/meteor/meteor/tree/release/METEOR%402.14/packages) + +--- + +## πŸ” External Authentication (OIDC, LDAP, etc.) + +WeKan supports external authentication methods via internal packages. + +πŸ“ See [`packages/`](https://github.com/wekan/wekan/tree/main/packages) for: +- OpenID Connect (OIDC) +- LDAP +- OAuth and other integrations + +--- + +## πŸ“¦ NPM & AtmosphereJS Dependencies + +- πŸ”— `package.json`: [Dependencies list](https://github.com/wekan/wekan/blob/main/package.json) +- 🧩 WekanTeam scoped NPM packages: [@wekanteam on npm](https://www.npmjs.com/search?q=%40wekanteam) +- ☁️ AtmosphereJS Meteor packages: [atmospherejs.com](https://atmospherejs.com) + +--- + +## 🚧 Meteor Version & Upgrade Notes + +- πŸ“Œ Current Version: **Meteor 2.14** + - [`.meteor/release`](https://github.com/wekan/wekan/blob/main/.meteor/release) +- πŸ”§ Maintained with only **critical fixes** until ~Summer 2025 +- πŸš€ Migration to **Meteor 3** or a new framework is under consideration + +πŸ“˜ Meteor 3 API: [docs.meteor.com/api/accounts](https://docs.meteor.com/api/accounts) + +--- + +## πŸ§ͺ Prototypes & Examples + +### 🐘 PHP Prototype Sign-Up + +Used in experimental versions: + +- Step 1: [`sign-up1.php`](https://github.com/wekan/php/blob/main/page/sign-up1.php) +- Step 2: [`sign-up2.php`](https://github.com/wekan/php/blob/main/page/sign-up2.php) +- Main entry: [`index.php#L72-L83`](https://github.com/wekan/php/blob/main/public/index.php#L72-L83) + +--- + +### 🎨 WeKan Studio Prototype + +Sign-up logic in the **WeKan Studio** version: + +- [`signUp.fmt`](https://github.com/wekan/wekanstudio/blob/main/srv/templates/login/signUp.fmt) + +--- + +## πŸ“Ž Future Considerations + +- Upgrading to **Meteor 3.x** +- Refactoring frontend logic to fix translation rendering order +- Exploring **simplified authentication systems** in future prototypes + +--- + +## πŸ”— Project Links + +- πŸ”§ Main Repo: [github.com/wekan/wekan](https://github.com/wekan/wekan) +- 🌐 Website: [wekan.github.io](https://wekan.github.io) +- πŸ“š Documentation: [Wekan Wiki](https://github.com/wekan/wekan/wiki) + +--- + + + +--- diff --git a/models/activities.js b/models/activities.js index ad3cb5f27..943fb9306 100644 --- a/models/activities.js +++ b/models/activities.js @@ -1,5 +1,15 @@ import { ReactiveCache } from '/imports/reactiveCache'; +// Activities don't need a schema because they are always set from the a trusted +// environment - the server - and there is no risk that a user change the logic +// we use with this collection. Moreover using a schema for this collection +// would be difficult (different activities have different fields) and wouldn't +// bring any direct advantage. +// +// XXX The activities API is not so nice and need some functionalities. For +// instance if a user archive a card, and un-archive it a few seconds later we +// should remove both activities assuming it was an error the user decided to +// revert. Activities = new Mongo.Collection('activities'); Activities.helpers({ @@ -42,14 +52,15 @@ Activities.helpers({ checklistItem() { return ReactiveCache.getChecklistItem(this.checklistItemId); }, - subtask() { + subtasks() { return ReactiveCache.getCard(this.subtaskId); }, customField() { return ReactiveCache.getCustomField(this.customFieldId); }, label() { - return ReactiveCache.getLabel(this.labelId); // Fixed: should get a label, not a card + // Label activity did not work yet, unable to edit labels when tried this. + return ReactiveCache.getCard(this.labelId); }, }); @@ -69,6 +80,9 @@ Activities.after.insert((userId, doc) => { }); if (Meteor.isServer) { + // For efficiency create indexes on the date of creation, and on the date of + // creation in conjunction with the card or board id, as corresponding views + // are largely used in the App. See #524. Meteor.startup(() => { Activities._collection.createIndex({ createdAt: -1 }); Activities._collection.createIndex({ modifiedAt: -1 }); @@ -86,6 +100,9 @@ if (Meteor.isServer) { { customFieldId: 1 }, { partialFilterExpression: { customFieldId: { $exists: true } } }, ); + // Label activity did not work yet, unable to edit labels when tried this. + //Activities._collection.dropIndex({ labelId: 1 }, { "indexKey": -1 }); + //Activities._collection.dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } }); }); Activities.after.insert((userId, doc) => { @@ -98,23 +115,36 @@ if (Meteor.isServer) { const params = { activityId: activity._id, }; - if (activity.userId) { + // No need send notification to user of activity + // participants = _.union(participants, [activity.userId]); const user = activity.user(); if (user) { - if (user.getName()) params.user = user.getName(); - if (user.emails) params.userEmails = user.emails; - params.userId = activity.userId; + if (user.getName()) { + params.user = user.getName(); + } + if (user.emails) { + params.userEmails = user.emails; + } + if (activity.userId) { + params.userId = activity.userId; + } } } - if (activity.boardId) { - params.board = board?.title || ''; + if (board.title) { + if (board.title.length > 0) { + params.board = board.title; + } else { + params.board = ''; + } + } else { + params.board = ''; + } title = 'act-withBoardTitle'; - params.url = board?.absoluteUrl(); + params.url = board.absoluteUrl(); params.boardId = activity.boardId; } - if (activity.oldBoardId) { const oldBoard = activity.oldBoard(); if (oldBoard) { @@ -123,22 +153,20 @@ if (Meteor.isServer) { params.oldBoardId = activity.oldBoardId; } } - if (activity.memberId) { participants = _.union(participants, [activity.memberId]); - const member = activity.member(); - if (member) params.member = member.getName(); + params.member = activity.member().getName(); } - if (activity.listId) { const list = activity.list(); if (list) { - watchers = _.union(watchers, list.watchers || []); + if (list.watchers !== undefined) { + watchers = _.union(watchers, list.watchers || []); + } params.list = list.title; params.listId = activity.listId; } } - if (activity.oldListId) { const oldList = activity.oldList(); if (oldList) { @@ -147,7 +175,6 @@ if (Meteor.isServer) { params.oldListId = activity.oldListId; } } - if (activity.oldSwimlaneId) { const oldSwimlane = activity.oldSwimlane(); if (oldSwimlane) { @@ -156,7 +183,6 @@ if (Meteor.isServer) { params.oldSwimlaneId = activity.oldSwimlaneId; } } - if (activity.cardId) { const card = activity.card(); participants = _.union(participants, [card.userId], card.members || []); @@ -166,102 +192,177 @@ if (Meteor.isServer) { params.url = card.absoluteUrl(); params.cardId = activity.cardId; } - if (activity.swimlaneId) { const swimlane = activity.swimlane(); - params.swimlane = swimlane?.title; + params.swimlane = swimlane.title; params.swimlaneId = activity.swimlaneId; } - if (activity.commentId) { const comment = activity.comment(); - if (comment) { - params.comment = comment.text; - params.commentId = comment._id; - - if (board) { - const knownUsers = board.members.map(member => { - const u = ReactiveCache.getUser(member.userId); - if (u) { - member.username = u.username; - member.emails = u.emails; - } - return member; - }); - - const mentionRegex = /\B@(?:(?:"([\w.\s-]*)")|([\w.-]+))/gi; - let currentMention; - while ((currentMention = mentionRegex.exec(comment.text)) !== null) { - const [ignored, quoteduser, simple] = currentMention; - const username = quoteduser || simple; - if (username === params.user) continue; - - if (username === 'board_members') { - const knownUids = knownUsers.map(u => u.userId); - watchers = _.union(watchers, knownUids); - title = 'act-atUserComment'; - } else if (username === 'card_members' && activity.cardId) { - const card = activity.card(); - watchers = _.union(watchers, card.members); - title = 'act-atUserComment'; - } else { - const atUser = _.findWhere(knownUsers, { username }); - if (atUser) { - params.atUsername = username; - params.atEmails = atUser.emails; - watchers = _.union(watchers, [atUser.userId]); - title = 'act-atUserComment'; - } - } + params.comment = comment.text; + if (board) { + const comment = params.comment; + const knownUsers = board.members.map(member => { + const u = ReactiveCache.getUser(member.userId); + if (u) { + member.username = u.username; + member.emails = u.emails; } + return member; + }); + const mentionRegex = /\B@(?:(?:"([\w.\s-]*)")|([\w.-]+))/gi; // including space in username + let currentMention; + while ((currentMention = mentionRegex.exec(comment)) !== null) { + /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "[iI]gnored" }]*/ + const [ignored, quoteduser, simple] = currentMention; + const username = quoteduser || simple; + if (username === params.user) { + // ignore commenter mention himself? + continue; + } + + if (activity.boardId && username === 'board_members') { + // mentions all board members + const knownUids = knownUsers.map(u => u.userId); + watchers = _.union(watchers, [...knownUids]); + title = 'act-atUserComment'; + } else if (activity.cardId && username === 'card_members') { + // mentions all card members if assigned + const card = activity.card(); + watchers = _.union(watchers, [...card.members]); + title = 'act-atUserComment'; + } else { + const atUser = _.findWhere(knownUsers, { username }); + if (!atUser) { + continue; + } + + const uid = atUser.userId; + params.atUsername = username; + params.atEmails = atUser.emails; + title = 'act-atUserComment'; + watchers = _.union(watchers, [uid]); + } + } } + params.commentId = comment._id; } - if (activity.attachmentId) { params.attachment = activity.attachmentName; params.attachmentId = activity.attachmentId; } - if (activity.checklistId) { const checklist = activity.checklist(); if (checklist) { - params.checklist = checklist.title; - params.checklistId = activity.checklistId; + if (checklist.title) { + params.checklist = checklist.title; + } } } - if (activity.checklistItemId) { const checklistItem = activity.checklistItem(); if (checklistItem) { - params.checklistItem = checklistItem.text; - params.checklistItemId = activity.checklistItemId; + if (checklistItem.title) { + params.checklistItem = checklistItem.title; + } } } - - if (activity.subtaskId) { - const subtask = activity.subtask(); - if (subtask) { - params.subtask = subtask.title; - params.subtaskId = activity.subtaskId; + if (activity.customFieldId) { + const customField = activity.customField(); + if (customField) { + if (customField.name) { + params.customField = customField.name; + } + if (activity.value) { + params.customFieldValue = activity.value; + } } } - + // Label activity did not work yet, unable to edit labels when tried this. if (activity.labelId) { const label = activity.label(); if (label) { - params.label = label.name || label.color; - params.labelId = label._id; + if (label.name) { + params.label = label.name; + } else if (label.color) { + params.label = label.color; + } + if (label._id) { + params.labelId = label._id; + } } } - - Notifications.insert({ - userId, - participants, - watchers, - title, - description, - params, + if ( + (!activity.timeKey || activity.timeKey === 'dueAt') && + activity.timeValue + ) { + // due time reminder, if it doesn't have old value, it's a brand new set, need some differentiation + title = activity.timeOldValue ? 'act-withDue' : 'act-newDue'; + } + ['timeValue', 'timeOldValue'].forEach(key => { + // copy time related keys & values to params + const value = activity[key]; + if (value) params[key] = value; }); + if (board) { + const BIGEVENTS = process.env.BIGEVENTS_PATTERN; // if environment BIGEVENTS_PATTERN is set, any activityType matching it is important + if (BIGEVENTS) { + try { + const atype = activity.activityType; + if (new RegExp(BIGEVENTS).exec(atype)) { + watchers = _.union( + watchers, + board.activeMembers().map(member => member.userId), + ); // notify all active members for important events + } + } catch (e) { + // passed env var BIGEVENTS_PATTERN is not a valid regex + } + } + + const watchingUsers = _.pluck( + _.where(board.watchers, { level: 'watching' }), + 'userId', + ); + const trackingUsers = _.pluck( + _.where(board.watchers, { level: 'tracking' }), + 'userId', + ); + watchers = _.union( + watchers, + watchingUsers, + _.intersection(participants, trackingUsers), + ); + } + Notifications.getUsers(watchers).forEach(user => { + // don't notify a user of their own behavior + if (user._id !== userId) { + Notifications.notify(user, title, description, params); + } + }); + + const integrations = ReactiveCache.getIntegrations({ + boardId: { $in: [board._id, Integrations.Const.GLOBAL_WEBHOOK_ID] }, + // type: 'outgoing-webhooks', // all types + enabled: true, + activities: { $in: [description, 'all'] }, + }); + if (integrations.length > 0) { + params.watchers = watchers; + integrations.forEach(integration => { + Meteor.call( + 'outgoingWebhooks', + integration, + description, + params, + () => { + return; + }, + ); + }); + } }); } + +export default Activities;