Marketing Hub: RBAC, i18n (AR/EN), tasks overhaul, team/user merge, tutorial

Features:
- Full RBAC with 3 roles (superadmin/manager/contributor)
- Ownership tracking on posts, tasks, campaigns, projects
- Task system: assign to anyone, filter combobox, visibility scoping
- Team members merged into users table (single source of truth)
- Post thumbnails on kanban cards from attachments
- Publication link validation before publishing
- Interactive onboarding tutorial with Settings restart
- Full Arabic/English i18n with RTL layout support
- Language toggle in sidebar, IBM Plex Sans Arabic font
- Brand-based visibility filtering for non-superadmins
- Manager can only create contributors
- Profile completion flow for new users
- Cookie-based sessions (express-session + SQLite)
This commit is contained in:
fahed
2026-02-08 20:46:58 +03:00
commit 35d84b6bff
2240 changed files with 846749 additions and 0 deletions

21
server/node_modules/connect-sqlite3/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 David Feinberg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

56
server/node_modules/connect-sqlite3/Readme.md generated vendored Normal file
View File

@@ -0,0 +1,56 @@
# Connect SQLite3
connect-sqlite3 is a SQLite3 session store modeled after the TJ's connect-redis store.
## Installation
```sh
$ npm install connect-sqlite3
```
## Options
- `table='sessions'` Database table name
- `db='sessionsDB'` Database file name (defaults to table name)
- `dir='.'` Directory to save '<db>.db' file
- `createDirIfNotExists='false'` Directory 'dir' is created recursively if not exists
- `concurrentDB='false'` Enables [WAL](https://www.sqlite.org/wal.html) mode (defaults to false)
## Usage
```js
var connect = require('connect'),
SQLiteStore = require('connect-sqlite3')(connect);
connect.createServer(
connect.cookieParser(),
connect.session({ store: new SQLiteStore, secret: 'your secret' })
);
```
with express
```js
3.x:
var SQLiteStore = require('connect-sqlite3')(express);
4.x:
var session = require('express-session');
var SQLiteStore = require('connect-sqlite3')(session);
app.configure(function() {
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(session({
store: new SQLiteStore,
secret: 'your secret',
cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 } // 1 week
}));
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
```
## Test
```sh
$ npm test
```

View File

@@ -0,0 +1,215 @@
/**
* Connect - SQLite3
* Copyright(c) 2012 David Feinberg
* MIT Licensed
* forked from https://github.com/tnantoka/connect-sqlite
*/
/**
* Module dependencies.
*/
var sqlite3 = require('sqlite3'),
events = require('events'),
fs = require('fs');
/**
* @type {Integer} One day in milliseconds.
*/
var oneDay = 86400000;
/**
* Return the SQLiteStore extending connect's session Store.
*
* @param {object} connect
* @return {Function}
* @api public
*/
module.exports = function(connect) {
/**
* Connect's Store.
*/
var Store = (connect.session) ? connect.session.Store : connect.Store;
/**
* Remove expired sessions from database.
* @param {Object} store
* @api private
*/
function dbCleanup(store) {
var now = new Date().getTime();
store.db.run('DELETE FROM ' + store.table + ' WHERE ? > expired', [now]);
}
/**
* Initialize SQLiteStore with the given options.
*
* @param {Object} options
* @api public
*/
function SQLiteStore(options) {
options = options || {};
Store.call(this, options);
this.table = options.table || 'sessions';
this.db = options.db || this.table;
var dbPath;
if (this.db.indexOf(':memory:') > -1 || this.db.indexOf('?mode=memory') > -1) {
dbPath = this.db;
} else {
dbPath = (options.dir || '.') + '/' + this.db;
}
if (options.dir && options.createDirIfNotExists) {
try {
fs.mkdirSync(options.dir,
{
recursive: true
});
}
catch {
}
}
this.db = new sqlite3.Database(dbPath, options.mode);
this.client = new events.EventEmitter();
var self = this;
this.db.exec((options.concurrentDb ? 'PRAGMA journal_mode = wal; ' : '') + 'CREATE TABLE IF NOT EXISTS ' + this.table + ' (' + 'sid PRIMARY KEY, ' + 'expired, sess)',
function(err) {
if (err) throw err;
self.client.emit('connect');
dbCleanup(self);
setInterval(dbCleanup, oneDay, self).unref();
}
);
}
/**
* Inherit from Store.
*/
SQLiteStore.prototype = Object.create(Store.prototype);
SQLiteStore.prototype.constructor = SQLiteStore;
/**
* Attempt to fetch session by the given sid.
*
* @param {String} sid
* @param {Function} fn
* @api public
*/
SQLiteStore.prototype.get = function(sid, fn) {
var now = new Date().getTime();
this.db.get('SELECT sess FROM ' + this.table + ' WHERE sid = ? AND ? <= expired', [sid, now],
function(err, row) {
if (err) fn(err);
if (!row) return fn();
fn(null, JSON.parse(row.sess));
}
);
};
/**
* Commit the given `sess` object associated with the given `sid`.
*
* @param {String} sid
* @param {Session} sess
* @param {Function} fn
* @api public
*/
SQLiteStore.prototype.set = function(sid, sess, fn) {
try {
var maxAge = sess.cookie.maxAge;
var now = new Date().getTime();
var expired = maxAge ? now + maxAge : now + oneDay;
sess = JSON.stringify(sess);
this.db.all('INSERT OR REPLACE INTO ' + this.table + ' VALUES (?, ?, ?)',
[sid, expired, sess],
function(err, rows) {
if (fn) fn.apply(this, arguments);
}
);
} catch (e) {
if (fn) fn(e);
}
};
/**
* Destroy the session associated with the given `sid`.
*
* @param {String} sid
* @api public
*/
SQLiteStore.prototype.destroy = function(sid, fn) {
this.db.run('DELETE FROM ' + this.table + ' WHERE sid = ?', [sid], fn);
};
/**
* Fetch all sessions.
*
* @param {Function} fn
* @api public
*/
SQLiteStore.prototype.all = function(fn) {
this.db.all('SELECT * FROM ' + this.table + '', function(err, rows) {
if (err) fn(err);
fn(null, rows.map((row) => JSON.parse(row.sess)));
});
};
/**
* Fetch number of sessions.
*
* @param {Function} fn
* @api public
*/
SQLiteStore.prototype.length = function(fn) {
this.db.all('SELECT COUNT(*) AS count FROM ' + this.table + '', function(err, rows) {
if (err) fn(err);
fn(null, rows[0].count);
});
};
/**
* Clear all sessions.
*
* @param {Function} fn
* @api public
*/
SQLiteStore.prototype.clear = function(fn) {
this.db.exec('DELETE FROM ' + this.table + '', function(err) {
if (err) fn(err);
fn(null, true);
});
};
/**
* Touch the given session object associated with the given session ID.
*
* @param {string} sid
* @param {object} session
* @param {function} fn
* @public
*/
SQLiteStore.prototype.touch = function(sid, session, fn) {
if (session && session.cookie && session.cookie.expires) {
var now = new Date().getTime();
var cookieExpires = new Date(session.cookie.expires).getTime();
this.db.run('UPDATE ' + this.table + ' SET expired=? WHERE sid = ? AND ? <= expired',
[cookieExpires, sid, now],
function(err) {
if (fn) {
if (err) fn(err);
fn(null, true);
}
}
);
} else {
fn(null, true);
}
}
return SQLiteStore;
};

31
server/node_modules/connect-sqlite3/package.json generated vendored Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "connect-sqlite3",
"description": "SQLite3 session store for Connect",
"version": "0.9.16",
"author": "David Feinberg",
"main": "lib/connect-sqlite3",
"scripts": {
"test": "mocha test/test.js"
},
"dependencies": {
"sqlite3": "^5.0.2"
},
"devDependencies": {
"connect": ">=1.0.0",
"express-session": "^1.13.0",
"mocha": "^2.3.4",
"should": "^8.2.0"
},
"engines": {
"node": ">=0.4.x"
},
"repository": {
"type": "git",
"url": "git://github.com/rawberg/connect-sqlite3.git"
},
"licenses": [
{
"type": "MIT"
}
]
}

182
server/node_modules/connect-sqlite3/test/mocha.css generated vendored Normal file
View File

@@ -0,0 +1,182 @@
body {
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
padding: 60px 50px;
}
#mocha ul, #mocha li {
margin: 0;
padding: 0;
}
#mocha ul {
list-style: none;
}
#mocha h1, #mocha h2 {
margin: 0;
}
#mocha h1 {
margin-top: 15px;
font-size: 1em;
font-weight: 200;
}
#mocha h1 a {
text-decoration: none;
color: inherit;
}
#mocha h1 a:hover {
text-decoration: underline;
}
#mocha .suite .suite h1 {
margin-top: 0;
font-size: .8em;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
}
#mocha .test:hover h2::after {
position: relative;
top: 0;
right: -10px;
content: '(view source)';
font-size: 12px;
font-family: arial;
color: #888;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial;
}
#mocha .test.pass.medium .duration {
background: #C09853;
}
#mocha .test.pass.slow .duration {
background: #B94A48;
}
#mocha .test.pass::before {
content: '✓';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #00d6b2;
}
#mocha .test.pass .duration {
font-size: 9px;
margin-left: 5px;
padding: 2px 5px;
color: white;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
border-radius: 5px;
}
#mocha .test.pass.fast .duration {
display: none;
}
#mocha .test.pending {
color: #0b97c4;
}
#mocha .test.pending::before {
content: '◦';
color: #0b97c4;
}
#mocha .test.fail {
color: #c00;
}
#mocha .test.fail pre {
color: black;
}
#mocha .test.fail::before {
content: '✖';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #c00;
}
#mocha .test pre.error {
color: #c00;
}
#mocha .test pre {
display: inline-block;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid #eee;
border-bottom-color: #ddd;
-webkit-border-radius: 3px;
-webkit-box-shadow: 0 1px 3px #eee;
}
#error {
color: #c00;
font-size: 1.5 em;
font-weight: 100;
letter-spacing: 1px;
}
#stats {
position: fixed;
top: 15px;
right: 10px;
font-size: 12px;
margin: 0;
color: #888;
}
#stats .progress {
float: right;
padding-top: 0;
}
#stats em {
color: black;
}
#stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
code .comment { color: #ddd }
code .init { color: #2F6FAD }
code .string { color: #5890AD }
code .keyword { color: #8A6343 }
code .number { color: #2F6FAD }

4504
server/node_modules/connect-sqlite3/test/mocha.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

124
server/node_modules/connect-sqlite3/test/test.js generated vendored Normal file
View File

@@ -0,0 +1,124 @@
var should = require('should'),
session = require('express-session'),
util = require('util'),
SQLiteStore = require('../lib/connect-sqlite3.js')(session);
describe('connect-sqlite3 basic test suite', function() {
before(function() {
this.memStore = new SQLiteStore({db: ':memory:', dir: 'dbs'});
});
after(function() {
//this.memStore.close();
});
it('it should save a new session record', function(done) {
this.memStore.set('1111222233334444', {cookie: {maxAge:2000}, name: 'sample name'}, function(err, rows) {
should.not.exist(err, 'set() returned an error');
rows.should.be.empty;
done();
});
});
it('it should retrieve an active session', function(done) {
this.memStore.get('1111222233334444', function(err, session) {
should.not.exist(err, 'get() returned an error');
should.exist(session);
(session).should.eql({cookie: {maxAge:2000}, name: 'sample name'});
done();
});
});
it('it should gracefully handle retrieving an unkonwn session', function(done) {
this.memStore.get('hope-and-change', function(err, rows) {
should.not.exist(err, 'get() unknown session returned an error');
should.equal(undefined, rows, 'unknown session is not undefined');
done();
});
});
it('it should only contain one session', function(done) {
this.memStore.length(function(err, len) {
should.not.exist(err, 'session count returned an error');
should.exist(len);
len.should.equal(1);
done();
});
});
it('it should clear all session records', function(done) {
var that = this;
this.memStore.clear(function(err, success) {
should.not.exist(err, 'clear returned an error');
success.should.be.true;
that.memStore.length(function(err, len) {
should.not.exist(err, 'session count after clear returned an error');
should.exist(len);
len.should.equal(0);
done();
});
});
});
it('it should destroy a session', function(done) {
var that = this;
this.memStore.set('555666777', {cookie: {maxAge:1000}, name: 'Rob Dobilina'}, function(err, rows) {
should.not.exist(err, 'set() returned an error');
rows.should.be.empty;
that.memStore.destroy('555666777', function(err) {
should.not.exist(err, 'destroy returned an error');
that.memStore.length(function(err, len) {
should.not.exist(err, 'session count after destroy returned an error');
should.exist(len);
len.should.equal(0);
done();
});
});
});
});
});
describe('connect-sqlite3 shared cache', function() {
it("should retrieve in one cached session what's stored in another.", function(done) {
var cwd = process.cwd();
var memStore = new SQLiteStore({db: 'file::memory:?cache=shared', mode: 0x20046});
process.chdir('..'); // Ensure we aren't opening a shared disk file
var memStore2 = new SQLiteStore({db: 'file::memory:?cache=shared', mode: 0x20046});
memStore.set('1111222233334444', {cookie: {maxAge:2011}, name: 'sample name'}, function(err, rows) {
process.chdir(cwd); // Restore dir
should.not.exist(err, 'set() returned an error');
rows.should.be.empty;
memStore2.get('1111222233334444', function(err, session) {
should.not.exist(err, 'get() returned an error');
should.exist(session);
(session).should.eql({cookie: {maxAge:2011}, name: 'sample name'});
done();
});
});
});
it("should not retrieve in one uncached session what's stored in another.", function(done) {
var memStore = new SQLiteStore({db: ':memory:'});
var memStore2 = new SQLiteStore({db: ':memory:'});
memStore.set('1111222233334444', {cookie: {maxAge:2011}, name: 'sample name'}, function(err, rows) {
should.not.exist(err, 'set() returned an error');
rows.should.be.empty;
memStore2.get('1111222233334444', function(err, session) {
should.not.exist(err, 'get() returned an error');
should.not.exist(session);
done();
});
});
});
});

16
server/node_modules/connect-sqlite3/test/tests.html generated vendored Normal file
View File

@@ -0,0 +1,16 @@
<html>
<head>
<title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="mocha.css" />
</head>
<body>
<div id="mocha"></div>
<script src="mocha.js"></script>
<script>mocha.setup('bdd')</script>
<script src="tests.js"></script>
<script>
mocha.run();
</script>
</body>
</html>