mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-11-27 03:57:21 +00:00
MWS authentication (#8596)
* mws authentication * add more tests and permission checkers * add logic to ensure that only authenticated users' requests are handled * add custom login page * Implement user authentication as well as session handling * work on user operations authorization * add middleware to route handlers for bags & tiddlers routes * add feature that only returns the tiddlers and bags which the user has permission to access on index page * refactor auth routes & added user management page * fix Ci Test failure issue * fix users list page, add manage roles page * add commands and scripts to create new user & assign roles and permissions * resolved ci-test failure * add ACL permissions to bags & tiddlers on creation * fix comments and access control list bug * fix indentation issues * working on user profile edit * remove list users command & added support for database in server options * implement user profile update and password change feature * update plugin readme * implement command which triggers protected mode on the server * revert server-wide auth flag. Implement selective authorization * ACL management feature * Complete Access control list implementation * Added support to manage users' assigned role by admin * fix comments * fix comment
This commit is contained in:
parent
5d6ddaee7e
commit
6a7612ddf8
@ -159,6 +159,8 @@ ListBullet/Caption: bulleted list
|
||||
ListBullet/Hint: Apply bulleted list formatting to lines containing selection
|
||||
ListNumber/Caption: numbered list
|
||||
ListNumber/Hint: Apply numbered list formatting to lines containing selection
|
||||
ManageACL/Caption: manage access control
|
||||
ManageACL/Hint: Manage access control configuration for this wiki
|
||||
MonoBlock/Caption: monospaced block
|
||||
MonoBlock/Hint: Apply monospaced block formatting to lines containing selection
|
||||
MonoLine/Caption: monospaced
|
||||
|
@ -44,7 +44,8 @@ NavigatorWidget.prototype.render = function(parent,nextSibling) {
|
||||
{type: "tm-fold-tiddler", handler: "handleFoldTiddlerEvent"},
|
||||
{type: "tm-fold-other-tiddlers", handler: "handleFoldOtherTiddlersEvent"},
|
||||
{type: "tm-fold-all-tiddlers", handler: "handleFoldAllTiddlersEvent"},
|
||||
{type: "tm-unfold-all-tiddlers", handler: "handleUnfoldAllTiddlersEvent"}
|
||||
{type: "tm-unfold-all-tiddlers", handler: "handleUnfoldAllTiddlersEvent"},
|
||||
{type: "tm-manage-acl", handler: "handleManageACLTiddlersEvent"}
|
||||
]);
|
||||
this.parentDomNode = parent;
|
||||
this.computeAttributes();
|
||||
@ -635,6 +636,14 @@ NavigatorWidget.prototype.handleUnfoldAllTiddlersEvent = function(event) {
|
||||
});
|
||||
};
|
||||
|
||||
NavigatorWidget.prototype.handleManageACLTiddlersEvent = function() {
|
||||
var pathname = window.location.pathname;
|
||||
var paths = pathname.split("/");
|
||||
var recipeName = paths[paths.length - 1];
|
||||
var bagName = document.querySelector("h1.tc-site-title").innerHTML;
|
||||
window.location.href = "/admin/acl/"+recipeName+"/"+bagName
|
||||
};
|
||||
|
||||
exports.navigator = NavigatorWidget;
|
||||
|
||||
})();
|
||||
|
15
core/ui/PageControls/manage-acl.tid
Normal file
15
core/ui/PageControls/manage-acl.tid
Normal file
@ -0,0 +1,15 @@
|
||||
title: $:/core/ui/Buttons/manage-acl
|
||||
tags: $:/tags/PageControls
|
||||
caption: {{$:/core/images/globe}} {{$:/language/Buttons/ManageACL/Caption}}
|
||||
description: {{$:/language/Buttons/ManageACL/Hint}}
|
||||
|
||||
\whitespace trim
|
||||
<$button tooltip={{$:/language/Buttons/ManageACL/Hint}} aria-label={{$:/language/Buttons/ManageACL/Caption}} class=<<tv-config-toolbar-class>>>
|
||||
<$action-managetiddler tiddler=<<currentTiddler>>/>
|
||||
{{$:/core/images/globe}}
|
||||
<%if [<tv-config-toolbar-text>match[yes]] %>
|
||||
<span class="tc-btn-text">
|
||||
<$text text={{$:/language/Buttons/ManageACL/Caption}}/>
|
||||
</span>
|
||||
<%endif%>
|
||||
</$button>
|
@ -1,2 +1,2 @@
|
||||
title: $:/tags/PageControls
|
||||
list: [[$:/core/ui/Buttons/home]] [[$:/core/ui/Buttons/close-all]] [[$:/core/ui/Buttons/fold-all]] [[$:/core/ui/Buttons/unfold-all]] [[$:/core/ui/Buttons/permaview]] [[$:/core/ui/Buttons/new-tiddler]] [[$:/core/ui/Buttons/new-journal]] [[$:/core/ui/Buttons/new-image]] [[$:/core/ui/Buttons/import]] [[$:/core/ui/Buttons/export-page]] [[$:/core/ui/Buttons/control-panel]] [[$:/core/ui/Buttons/advanced-search]] [[$:/core/ui/Buttons/manager]] [[$:/core/ui/Buttons/tag-manager]] [[$:/core/ui/Buttons/language]] [[$:/core/ui/Buttons/palette]] [[$:/core/ui/Buttons/theme]] [[$:/core/ui/Buttons/layout]] [[$:/core/ui/Buttons/storyview]] [[$:/core/ui/Buttons/encryption]] [[$:/core/ui/Buttons/timestamp]] [[$:/core/ui/Buttons/full-screen]] [[$:/core/ui/Buttons/print]] [[$:/core/ui/Buttons/save-wiki]] [[$:/core/ui/Buttons/refresh]] [[$:/core/ui/Buttons/network-activity]] [[$:/core/ui/Buttons/more-page-actions]]
|
||||
list: [[$:/core/ui/Buttons/home]] [[$:/core/ui/Buttons/close-all]] [[$:/core/ui/Buttons/fold-all]] [[$:/core/ui/Buttons/unfold-all]] [[$:/core/ui/Buttons/permaview]] [[$:/core/ui/Buttons/new-tiddler]] [[$:/core/ui/Buttons/new-journal]] [[$:/core/ui/Buttons/new-image]] [[$:/core/ui/Buttons/import]] [[$:/core/ui/Buttons/export-page]] [[$:/core/ui/Buttons/control-panel]] [[$:/core/ui/Buttons/advanced-search]] [[$:/core/ui/Buttons/manager]] [[$:/core/ui/Buttons/tag-manager]] [[$:/core/ui/Buttons/language]] [[$:/core/ui/Buttons/palette]] [[$:/core/ui/Buttons/theme]] [[$:/core/ui/Buttons/layout]] [[$:/core/ui/Buttons/storyview]] [[$:/core/ui/Buttons/encryption]] [[$:/core/ui/Buttons/manage-acl]] [[$:/core/ui/Buttons/timestamp]] [[$:/core/ui/Buttons/full-screen]] [[$:/core/ui/Buttons/print]] [[$:/core/ui/Buttons/save-wiki]] [[$:/core/ui/Buttons/refresh]] [[$:/core/ui/Buttons/network-activity]] [[$:/core/ui/Buttons/more-page-actions]]
|
||||
|
@ -11,6 +11,15 @@
|
||||
"tiddlywiki/snowwhite"
|
||||
],
|
||||
"build": {
|
||||
"mws-add-user": [
|
||||
"--mws-add-permission", "READ", "Allows user to create tiddlers",
|
||||
"--mws-add-permission", "WRITE", "Gives the user the permission to edit and delete tiddlers",
|
||||
"--mws-add-role", "ADMIN", "System Administrator",
|
||||
"--mws-assign-role-permission", "ADMIN", "READ",
|
||||
"--mws-assign-role-permission", "ADMIN", "WRITE",
|
||||
"--mws-add-user", "user", "pass123",
|
||||
"--mws-assign-user-role", "user", "ADMIN"
|
||||
],
|
||||
"load-mws-demo-data": [
|
||||
"--mws-load-wiki-folder","./editions/tw5.com","docs", "TiddlyWiki Documentation from https://tiddlywiki.com","docs","TiddlyWiki Documentation from https://tiddlywiki.com",
|
||||
"--mws-load-wiki-folder","./editions/dev","dev","TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev","dev-docs", "TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev",
|
||||
@ -25,7 +34,14 @@
|
||||
"--mws-save-tiddler-text","bag-alpha","$:/SiteTitle","bag-alpha",
|
||||
"--mws-save-tiddler-text","bag-alpha","😀😃😄😁😆🥹😅😂","bag-alpha",
|
||||
"--mws-save-tiddler-text","bag-beta","$:/SiteTitle","bag-beta",
|
||||
"--mws-save-tiddler-text","bag-gamma","$:/SiteTitle","bag-gamma"
|
||||
"--mws-save-tiddler-text","bag-gamma","$:/SiteTitle","bag-gamma",
|
||||
"--mws-add-permission", "READ", "Allows user to create tiddlers",
|
||||
"--mws-add-permission", "WRITE", "Gives the user the permission to edit and delete tiddlers",
|
||||
"--mws-add-role", "ADMIN", "System Administrator",
|
||||
"--mws-add-role", "USER", "Basic User",
|
||||
"--mws-assign-role-permission", "ADMIN", "READ",
|
||||
"--mws-assign-role-permission", "ADMIN", "WRITE",
|
||||
"--mws-assign-role-permission", "USER", "READ"
|
||||
]
|
||||
}
|
||||
}
|
26
package-lock.json
generated
26
package-lock.json
generated
@ -9,10 +9,10 @@
|
||||
"version": "5.3.6-prerelease",
|
||||
"license": "BSD",
|
||||
"dependencies": {
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@playwright/test": "^1.47.2",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"node-sqlite3-wasm": "^0.8.10",
|
||||
"playwright": "^1.46.1"
|
||||
"playwright": "^1.47.2"
|
||||
},
|
||||
"bin": {
|
||||
"tiddlywiki": "tiddlywiki.js"
|
||||
@ -177,11 +177,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz",
|
||||
"integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==",
|
||||
"version": "1.47.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
|
||||
"integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
|
||||
"dependencies": {
|
||||
"playwright": "1.46.1"
|
||||
"playwright": "1.47.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -1205,11 +1205,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.46.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz",
|
||||
"integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==",
|
||||
"version": "1.47.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
|
||||
"integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.46.1"
|
||||
"playwright-core": "1.47.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -1222,9 +1222,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.46.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz",
|
||||
"integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==",
|
||||
"version": "1.47.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
|
||||
"integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
|
@ -35,14 +35,15 @@
|
||||
"start": "node ./tiddlywiki.js ./editions/multiwikiserver --mws-load-plugin-bags --build load-mws-demo-data --mws-listen",
|
||||
"build:test-edition": "node ./tiddlywiki.js ./editions/test --verbose --version --build index",
|
||||
"test:multiwikiserver-edition": "node ./tiddlywiki.js ./editions/multiwikiserver/ --build load-mws-demo-data --mws-listen --mws-test-server http://127.0.0.1:8080/ --quit",
|
||||
"mws-add-user": "node ./tiddlywiki.js ./editions/multiwikiserver --build load-mws-demo-data --mws-listen --build mws-add-user --quit",
|
||||
"test": "npm run build:test-edition && npm run test:multiwikiserver-edition",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@playwright/test": "^1.47.2",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"node-sqlite3-wasm": "^0.8.10",
|
||||
"playwright": "^1.46.1"
|
||||
"playwright": "^1.47.2"
|
||||
}
|
||||
}
|
||||
|
65
plugins/tiddlywiki/multiwikiclient/managetiddleraction.js
Normal file
65
plugins/tiddlywiki/multiwikiclient/managetiddleraction.js
Normal file
@ -0,0 +1,65 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiclient/managetiddleraction.js
|
||||
type: application/javascript
|
||||
module-type: widget
|
||||
|
||||
A widget to manage tiddler actions.
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var Widget = require("$:/core/modules/widgets/widget.js").widget;
|
||||
|
||||
var ManageTiddlerAction = function(parseTreeNode,options) {
|
||||
this.initialise(parseTreeNode,options);
|
||||
};
|
||||
|
||||
/*
|
||||
Inherit from the base widget class
|
||||
*/
|
||||
ManageTiddlerAction.prototype = new Widget();
|
||||
|
||||
/*
|
||||
Render this widget into the DOM
|
||||
*/
|
||||
ManageTiddlerAction.prototype.render = function(parent,nextSibling) {
|
||||
this.computeAttributes();
|
||||
this.execute();
|
||||
};
|
||||
|
||||
/*
|
||||
Compute the internal state of the widget
|
||||
*/
|
||||
ManageTiddlerAction.prototype.execute = function() {
|
||||
this.tiddler = this.getAttribute("tiddler");
|
||||
};
|
||||
|
||||
/*
|
||||
Invoke the action associated with this widget
|
||||
*/
|
||||
ManageTiddlerAction.prototype.invokeAction = function(triggeringWidget,event) {
|
||||
var pathname = window.location.pathname;
|
||||
var paths = pathname.split("/");
|
||||
var recipeName = paths[paths.length - 1];
|
||||
var bagName = document.querySelector("h1.tc-site-title").innerHTML;
|
||||
window.location.href = "/admin/acl/"+recipeName+"/"+bagName;
|
||||
};
|
||||
|
||||
/*
|
||||
Refresh the widget by ensuring our attributes are up to date
|
||||
*/
|
||||
ManageTiddlerAction.prototype.refresh = function(changedTiddlers) {
|
||||
var changedAttributes = this.computeAttributes();
|
||||
if(changedAttributes.tiddler) {
|
||||
this.refreshSelf();
|
||||
return true;
|
||||
}
|
||||
return this.refreshChildren(changedTiddlers);
|
||||
};
|
||||
|
||||
exports["action-managetiddler"] = ManageTiddlerAction;
|
||||
|
||||
})();
|
43
plugins/tiddlywiki/multiwikiserver/auth/authentication.js
Normal file
43
plugins/tiddlywiki/multiwikiserver/auth/authentication.js
Normal file
@ -0,0 +1,43 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js
|
||||
type: application/javascript
|
||||
module-type: library
|
||||
|
||||
Handles authentication related operations
|
||||
|
||||
\*/
|
||||
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var crypto = require("crypto");
|
||||
|
||||
function Authenticator(database) {
|
||||
if(!(this instanceof Authenticator)) {
|
||||
return new Authenticator(database);
|
||||
}
|
||||
this.sqlTiddlerDatabase = database;
|
||||
}
|
||||
|
||||
Authenticator.prototype.verifyPassword = function(inputPassword, storedHash) {
|
||||
var hashedInput = this.hashPassword(inputPassword);
|
||||
return hashedInput === storedHash;
|
||||
};
|
||||
|
||||
Authenticator.prototype.hashPassword = function(password) {
|
||||
return crypto.createHash("sha256").update(password).digest("hex");
|
||||
};
|
||||
|
||||
Authenticator.prototype.createSession = function(userId) {
|
||||
var sessionId = crypto.randomBytes(16).toString("hex");
|
||||
// Store the session in your database or in-memory store
|
||||
this.sqlTiddlerDatabase.createOrUpdateUserSession(userId, sessionId);
|
||||
return sessionId;
|
||||
};
|
||||
|
||||
exports.Authenticator = Authenticator;
|
||||
|
||||
})();
|
19
plugins/tiddlywiki/multiwikiserver/auth/form/login.tid
Normal file
19
plugins/tiddlywiki/multiwikiserver/auth/form/login.tid
Normal file
@ -0,0 +1,19 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login
|
||||
tags: $:/tags/ServerRoute
|
||||
route-method: GET
|
||||
route-path: /login
|
||||
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/styles"/>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/head"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/header" mode="block"/>
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/form" mode="block"/>
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/auth/form/login/error-message" mode="block"/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,7 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/error-message
|
||||
|
||||
<$list filter="[[$:/temp/mws/login/error]!is[missing]]" variable="errorTiddler">
|
||||
<div class="tc-error-message">
|
||||
{{$:/temp/mws/login/error}}
|
||||
</div>
|
||||
</$list>
|
@ -0,0 +1,8 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/form
|
||||
|
||||
<form class="login-form" method="POST" action="/login">
|
||||
<input type="hidden" name="returnUrl" value=<<returnUrl>>/>
|
||||
<input type="text" name="username" placeholder="Username"/>
|
||||
<input type="password" name="password" placeholder="Password"/>
|
||||
<input type="submit" value="Log In"/>
|
||||
</form>
|
@ -0,0 +1,3 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/head
|
||||
|
||||
<title>TiddlyWiki Login</title>
|
@ -0,0 +1,3 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/header
|
||||
|
||||
<h1>TiddlyWiki Login</h1>
|
@ -0,0 +1,48 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/styles
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.login-container {
|
||||
max-width: 300px;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.login-container h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.login-form input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.login-form input[type="submit"] {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
.login-form input[type="submit"]:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.tc-error-message {
|
||||
color: #ff0000;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
@ -33,3 +33,99 @@ To run the tests:
|
||||
```
|
||||
./bin/test.sh
|
||||
```
|
||||
|
||||
# Authentication & Authorization
|
||||
|
||||
## Overview
|
||||
|
||||
Our application has transitioned from a conventional username/password authentication system to a more robust Authentication and Authorization implementation. This new system supports multiple user accounts, roles, permissions, and a comprehensive access control list.
|
||||
|
||||
## Key Features
|
||||
|
||||
1. Multiple User Accounts
|
||||
2. Role-based Access Control
|
||||
3. Granular Permissions
|
||||
4. Access Control List (ACL)
|
||||
|
||||
## Running the App
|
||||
By default, the Multiwiki Server starts up without the need for authentication. An admin user is created by default with the username `user` and password `pass123`.
|
||||
This user has the `ADMIN` role which grants full access to all recipes. This user is automatically assigned the `READ` and `WRITE` permissions.
|
||||
You can create additional users and roles through the admin interface.
|
||||
|
||||
Access control is enforced at the recipe level. A user can read or write to a recipe if they have the `READ` or `WRITE` permission for that recipe.
|
||||
Roles can be assigned to users and can have the `READ` and `WRITE` permissions assigned to them.
|
||||
|
||||
Guest users have no permissions and can only access recipes that do not have an ACL assigned to them.
|
||||
|
||||
To start the app, run the following command:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
|
||||
## Setting Up the Admin User
|
||||
|
||||
To initialize the system with an admin user, use the following command:
|
||||
|
||||
```bash
|
||||
npm run mws-add-user
|
||||
```
|
||||
|
||||
This command performs the following actions:
|
||||
- Creates a default admin user with the credentials:
|
||||
- Username: `user`
|
||||
- Password: `pass123`
|
||||
- Establishes `READ` and `WRITE` permissions
|
||||
- Creates an `ADMIN` role
|
||||
- Assigns `READ` and `WRITE` permissions to the `ADMIN` role
|
||||
- Assigns the `ADMIN` role to the newly created user
|
||||
|
||||
## User Management
|
||||
|
||||
Users can be managed through the application interface or via command-line tools. Each user account consists of:
|
||||
- Unique username
|
||||
- Securely hashed password
|
||||
- Assigned role(s)
|
||||
|
||||
## Roles and Permissions
|
||||
|
||||
### Roles
|
||||
|
||||
Roles are used to group sets of permissions. The default setup includes an `ADMIN` role, but additional roles can be created as needed (e.g., `USER`, `MANAGER`).
|
||||
|
||||
### Permissions
|
||||
|
||||
Permissions define specific actions that can be performed. The default permissions are:
|
||||
- `READ`: Allows viewing of resources
|
||||
- `WRITE`: Allows creation, modification, and deletion of resources
|
||||
|
||||
Additional permissions can be created to suit specific application needs.
|
||||
|
||||
## Access Control List (ACL)
|
||||
|
||||
The ACL maintains a mapping between roles, permissions, and entities (such as recipes or bags). This allows for fine-grained control over who can perform what actions on which resources.
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. User provides credentials (username and password)
|
||||
2. System verifies credentials against stored user data
|
||||
3. If valid, a session is created for the user
|
||||
|
||||
## Authorization Flow
|
||||
|
||||
1. User attempts to perform an action on a resource
|
||||
2. System checks the user's role
|
||||
3. System verifies if the role has the required permission for the action
|
||||
4. System checks the ACL to ensure the role has permission for the specific entity
|
||||
5. If all checks pass, the action is allowed; otherwise, it's denied
|
||||
|
||||
## Extending the System
|
||||
|
||||
To add new roles, permissions, or modify the ACL:
|
||||
|
||||
1. Use the provided administrative interface or CLI tools
|
||||
2. Update the ACL structure in the application's configuration
|
||||
3. Implement new permission checks in the relevant parts of the application
|
||||
|
||||
By following this documentation, you can effectively manage and utilize the new Authentication and Authorization system in your application.
|
@ -0,0 +1,49 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-add-permission.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Command to create a permission
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.info = {
|
||||
name: "mws-add-permission",
|
||||
synchronous: false
|
||||
};
|
||||
|
||||
var Command = function(params,commander,callback) {
|
||||
this.params = params;
|
||||
this.commander = commander;
|
||||
this.callback = callback;
|
||||
};
|
||||
|
||||
Command.prototype.execute = function() {
|
||||
var self = this;
|
||||
|
||||
if(this.params.length < 2) {
|
||||
return "Usage: --mws-add-permission <permission_name> <description>";
|
||||
}
|
||||
|
||||
if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) {
|
||||
return "Error: MultiWikiServer or SQL database not initialized.";
|
||||
}
|
||||
|
||||
var permission_name = this.params[0];
|
||||
var description = this.params[1];
|
||||
|
||||
$tw.mws.store.sqlTiddlerDatabase.createPermission(permission_name, description);
|
||||
|
||||
console.log(permission_name+" Permission Created Successfully!")
|
||||
self.callback();
|
||||
return null;
|
||||
};
|
||||
|
||||
exports.Command = Command;
|
||||
|
||||
})();
|
@ -0,0 +1,49 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-add-role.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Command to create a role
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.info = {
|
||||
name: "mws-add-role",
|
||||
synchronous: false
|
||||
};
|
||||
|
||||
var Command = function(params,commander,callback) {
|
||||
this.params = params;
|
||||
this.commander = commander;
|
||||
this.callback = callback;
|
||||
};
|
||||
|
||||
Command.prototype.execute = function() {
|
||||
var self = this;
|
||||
|
||||
if(this.params.length < 2) {
|
||||
return "Usage: --mws-add-role <role_name> <description>";
|
||||
}
|
||||
|
||||
if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) {
|
||||
return "Error: MultiWikiServer or SQL database not initialized.";
|
||||
}
|
||||
|
||||
var role_name = this.params[0];
|
||||
var description = this.params[1];
|
||||
|
||||
$tw.mws.store.sqlTiddlerDatabase.createRole(role_name, description);
|
||||
|
||||
console.log(role_name+" Role Created Successfully!")
|
||||
self.callback(null, "Role Created Successfully!");
|
||||
return null;
|
||||
};
|
||||
|
||||
exports.Command = Command;
|
||||
|
||||
})();
|
@ -0,0 +1,58 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-add-user.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Command to create users and grant permission
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
if($tw.node) {
|
||||
var crypto = require("crypto");
|
||||
}
|
||||
exports.info = {
|
||||
name: "mws-add-user",
|
||||
synchronous: false
|
||||
};
|
||||
|
||||
var Command = function(params,commander,callback) {
|
||||
this.params = params;
|
||||
this.commander = commander;
|
||||
this.callback = callback;
|
||||
};
|
||||
|
||||
Command.prototype.execute = function() {
|
||||
var self = this;
|
||||
|
||||
if(this.params.length < 2) {
|
||||
return "Usage: --mws-add-user <username> <password> [email]";
|
||||
}
|
||||
|
||||
if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) {
|
||||
return "Error: MultiWikiServer or SQL database not initialized.";
|
||||
}
|
||||
|
||||
var username = this.params[0];
|
||||
var password = this.params[1];
|
||||
var email = this.params[2] || username + "@example.com";
|
||||
var hashedPassword = crypto.createHash("sha256").update(password).digest("hex");
|
||||
|
||||
var user = $tw.mws.store.sqlTiddlerDatabase.getUserByUsername(username);
|
||||
|
||||
if(user) {
|
||||
self.callback("WARNING: An account with the username (" + username + ") already exists");
|
||||
} else {
|
||||
$tw.mws.store.sqlTiddlerDatabase.createUser(username, email, hashedPassword);
|
||||
console.log("User Account Created Successfully!")
|
||||
self.callback();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
exports.Command = Command;
|
||||
|
||||
})();
|
@ -0,0 +1,62 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-assign-role-permission.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Command to assign permission to a role
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.info = {
|
||||
name: "mws-assign-role-permission",
|
||||
synchronous: false
|
||||
};
|
||||
|
||||
var Command = function(params,commander,callback) {
|
||||
this.params = params;
|
||||
this.commander = commander;
|
||||
this.callback = callback;
|
||||
};
|
||||
|
||||
Command.prototype.execute = function() {
|
||||
var self = this;
|
||||
|
||||
if(this.params.length < 2) {
|
||||
return "Usage: --mws-assign-role-permission <role_name> <permission_name>";
|
||||
}
|
||||
|
||||
if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) {
|
||||
return "Error: MultiWikiServer or SQL database not initialized.";
|
||||
}
|
||||
|
||||
var role_name = this.params[0];
|
||||
var permission_name = this.params[1];
|
||||
var role = $tw.mws.store.sqlTiddlerDatabase.getRoleByName(role_name);
|
||||
var permission = $tw.mws.store.sqlTiddlerDatabase.getPermissionByName(permission_name);
|
||||
|
||||
if(!role) {
|
||||
return "Error: Unable to find Role: "+role_name;
|
||||
}
|
||||
|
||||
if(!permission) {
|
||||
return "Error: Unable to find Permission: "+permission_name;
|
||||
}
|
||||
|
||||
var permission = $tw.mws.store.sqlTiddlerDatabase.getPermissionByName(permission_name);
|
||||
|
||||
|
||||
$tw.mws.store.sqlTiddlerDatabase.addPermissionToRole(role.role_id, permission.permission_id);
|
||||
|
||||
console.log(permission_name+" permission assigned to "+role_name+" role successfully!")
|
||||
self.callback();
|
||||
return null;
|
||||
};
|
||||
|
||||
exports.Command = Command;
|
||||
|
||||
})();
|
@ -0,0 +1,59 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-assign-user-role.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Command to assign a role to a user
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.info = {
|
||||
name: "mws-assign-user-role",
|
||||
synchronous: false
|
||||
};
|
||||
|
||||
var Command = function(params,commander,callback) {
|
||||
this.params = params;
|
||||
this.commander = commander;
|
||||
this.callback = callback;
|
||||
};
|
||||
|
||||
Command.prototype.execute = function() {
|
||||
var self = this;
|
||||
|
||||
if(this.params.length < 2) {
|
||||
return "Usage: --mws-assign-user-role <username> <role_name>";
|
||||
}
|
||||
|
||||
if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) {
|
||||
return "Error: MultiWikiServer or SQL database not initialized.";
|
||||
}
|
||||
|
||||
var username = this.params[0];
|
||||
var role_name = this.params[1];
|
||||
var role = $tw.mws.store.sqlTiddlerDatabase.getRoleByName(role_name);
|
||||
var user = $tw.mws.store.sqlTiddlerDatabase.getUserByUsername(username);
|
||||
|
||||
if(!role) {
|
||||
return "Error: Unable to find Role: "+role_name;
|
||||
}
|
||||
|
||||
if(!user) {
|
||||
return "Error: Unable to find user with the username "+username;
|
||||
}
|
||||
|
||||
$tw.mws.store.sqlTiddlerDatabase.addRoleToUser(user.user_id, role.role_id);
|
||||
|
||||
console.log(role_name+" role has been assigned to user with username "+username)
|
||||
self.callback();
|
||||
return null;
|
||||
};
|
||||
|
||||
exports.Command = Command;
|
||||
|
||||
})();
|
@ -50,11 +50,18 @@ TestRunner.prototype.runTests = function(callback) {
|
||||
const self = this;
|
||||
let currentTestSpec = 0;
|
||||
let hasFailed = false;
|
||||
let sessionId;
|
||||
function runNextTest() {
|
||||
if(currentTestSpec < testSpecs.length) {
|
||||
const testSpec = testSpecs[currentTestSpec];
|
||||
if(!!sessionId) {
|
||||
testSpec.headers['Cookie'] = `session=${sessionId}; HttpOnly; Path=/`;
|
||||
}
|
||||
currentTestSpec += 1;
|
||||
self.runTest(testSpec,function(err) {
|
||||
self.runTest(testSpec,function(err, data) {
|
||||
if(data?.sessionId) {
|
||||
sessionId = data?.sessionId;
|
||||
}
|
||||
if(err) {
|
||||
hasFailed = true;
|
||||
console.log(`Failed "${testSpec.description}" with "${err}"`)
|
||||
@ -96,7 +103,7 @@ TestRunner.prototype.runTest = function(testSpec,callback) {
|
||||
response.on("end", () => {
|
||||
const jsonData = $tw.utils.parseJSONSafe(buffer,function() {return undefined;});
|
||||
const testResult = testSpec.expectedResult(jsonData,buffer,response.headers);
|
||||
callback(testResult ? null : "Test failed");
|
||||
callback(testResult ? null : "Test failed", jsonData);
|
||||
});
|
||||
});
|
||||
request.on("error", (e) => {
|
||||
@ -112,6 +119,20 @@ TestRunner.prototype.runTest = function(testSpec,callback) {
|
||||
};
|
||||
|
||||
const testSpecs = [
|
||||
{
|
||||
description: "Login Test User",
|
||||
method: "POST",
|
||||
path: "/login",
|
||||
headers: {
|
||||
"Accept": 'application/json',
|
||||
"Content-Type": 'application/x-www-form-urlencoded',
|
||||
"User-Agent": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
|
||||
},
|
||||
data: "username=user&password=pass123",
|
||||
expectedResult: (jsonData,data,headers) => {
|
||||
return !!jsonData.sessionId;
|
||||
}
|
||||
},
|
||||
{
|
||||
description: "Check index page",
|
||||
method: "GET",
|
||||
|
@ -19,7 +19,8 @@ if($tw.node) {
|
||||
path = require("path"),
|
||||
querystring = require("querystring"),
|
||||
crypto = require("crypto"),
|
||||
zlib = require("zlib");
|
||||
zlib = require("zlib"),
|
||||
aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -34,6 +35,7 @@ function Server(options) {
|
||||
this.authenticators = options.authenticators || [];
|
||||
this.wiki = options.wiki;
|
||||
this.boot = options.boot || $tw.boot;
|
||||
this.sqlTiddlerDatabase = options.sqlTiddlerDatabase || $tw.mws.store.sqlTiddlerDatabase;
|
||||
// Initialise the variables
|
||||
this.variables = $tw.utils.extend({},this.defaultVariables);
|
||||
if(options.variables) {
|
||||
@ -157,9 +159,10 @@ function sendResponse(request,response,statusCode,headers,data,encoding) {
|
||||
data = zlib.gzipSync(data);
|
||||
}
|
||||
}
|
||||
|
||||
response.writeHead(statusCode,headers);
|
||||
response.end(data,encoding);
|
||||
if(!response.headersSent) {
|
||||
response.writeHead(statusCode,headers);
|
||||
response.end(data,encoding);
|
||||
}
|
||||
}
|
||||
|
||||
function redirect(request,response,statusCode,location) {
|
||||
@ -350,6 +353,13 @@ Server.prototype.methodMappings = {
|
||||
"DELETE": "writers"
|
||||
};
|
||||
|
||||
Server.prototype.methodACLPermMappings = {
|
||||
"GET": "READ",
|
||||
"PUT": "WRITE",
|
||||
"POST": "WRITE",
|
||||
"DELETE": "WRITE"
|
||||
}
|
||||
|
||||
/*
|
||||
Check whether a given user is authorized for the specified authorizationType ("readers" or "writers"). Pass null or undefined as the username to check for anonymous access
|
||||
*/
|
||||
@ -358,9 +368,57 @@ Server.prototype.isAuthorized = function(authorizationType,username) {
|
||||
return principals.indexOf("(anon)") !== -1 || (username && (principals.indexOf("(authenticated)") !== -1 || principals.indexOf(username) !== -1));
|
||||
}
|
||||
|
||||
Server.prototype.parseCookieString = function(cookieString) {
|
||||
const cookies = {};
|
||||
if (typeof cookieString !== 'string') return cookies;
|
||||
|
||||
cookieString.split(';').forEach(cookie => {
|
||||
const parts = cookie.split('=');
|
||||
if (parts.length >= 2) {
|
||||
const key = parts[0].trim();
|
||||
const value = parts.slice(1).join('=').trim();
|
||||
cookies[key] = decodeURIComponent(value);
|
||||
}
|
||||
});
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
Server.prototype.authenticateUser = function(request, response) {
|
||||
const {session: session_id} = this.parseCookieString(request.headers.cookie)
|
||||
if (!session_id) {
|
||||
return false;
|
||||
}
|
||||
// get user info
|
||||
const user = this.sqlTiddlerDatabase.findUserBySessionId(session_id);
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
delete user.password;
|
||||
const userRole = this.sqlTiddlerDatabase.getUserRoles(user.user_id);
|
||||
user['isAdmin'] = userRole?.role_name?.toLowerCase() === 'admin'
|
||||
|
||||
return user
|
||||
};
|
||||
|
||||
Server.prototype.requestAuthentication = function(response) {
|
||||
if(!response.headersSent) {
|
||||
response.writeHead(401, {
|
||||
'WWW-Authenticate': 'Basic realm="Secure Area"'
|
||||
});
|
||||
response.end('Authentication required.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Server.prototype.requestHandler = function(request,response,options) {
|
||||
options = options || {};
|
||||
const queryString = require("querystring");
|
||||
|
||||
// Authenticate the user
|
||||
const authenticatedUser = this.authenticateUser(request, response);
|
||||
const authenticatedUsername = authenticatedUser?.username;
|
||||
|
||||
// Compose the state object
|
||||
var self = this;
|
||||
var state = {};
|
||||
@ -374,43 +432,52 @@ Server.prototype.requestHandler = function(request,response,options) {
|
||||
state.redirect = redirect.bind(self,request,response);
|
||||
state.streamMultipartData = streamMultipartData.bind(self,request);
|
||||
state.makeTiddlerEtag = makeTiddlerEtag.bind(self);
|
||||
state.authenticatedUser = authenticatedUser;
|
||||
state.authenticatedUsername = authenticatedUsername;
|
||||
|
||||
// Get the principals authorized to access this resource
|
||||
state.authorizationType = options.authorizationType || this.methodMappings[request.method] || "readers";
|
||||
|
||||
// Check whether anonymous access is granted
|
||||
state.allowAnon = this.isAuthorized(state.authorizationType,null);
|
||||
// Authenticate with the first active authenticator
|
||||
if(this.authenticators.length > 0) {
|
||||
if(!this.authenticators[0].authenticateRequest(request,response,state)) {
|
||||
// Bail if we failed (the authenticator will have sent the response)
|
||||
return;
|
||||
}
|
||||
}
|
||||
state.allowAnon = false; //this.isAuthorized(state.authorizationType,null);
|
||||
|
||||
// Authorize with the authenticated username
|
||||
if(!this.isAuthorized(state.authorizationType,state.authenticatedUsername)) {
|
||||
response.writeHead(401,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'");
|
||||
if(!this.isAuthorized(state.authorizationType,state.authenticatedUsername) && !response.headersSent) {
|
||||
response.writeHead(403,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'");
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the route that matches this path
|
||||
var route = self.findMatchingRoute(request,state);
|
||||
|
||||
// If the route is configured to use ACL middleware, check that the user has permission
|
||||
if(route?.useACL) {
|
||||
const permissionName = this.methodACLPermMappings[route.method];
|
||||
aclMiddleware(request,response,state,route.entityName,permissionName)
|
||||
}
|
||||
|
||||
// Optionally output debug info
|
||||
if(self.get("debug-level") !== "none") {
|
||||
console.log("Request path:",JSON.stringify(state.urlInfo));
|
||||
console.log("Request headers:",JSON.stringify(request.headers));
|
||||
console.log("authenticatedUsername:",state.authenticatedUsername);
|
||||
}
|
||||
|
||||
// Return a 404 if we didn't find a route
|
||||
if(!route) {
|
||||
if(!route && !response.headersSent) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is a write, check for the CSRF header unless globally disabled, or disabled for this route
|
||||
if(!this.csrfDisable && !route.csrfDisable && state.authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki") {
|
||||
if(!this.csrfDisable && !route.csrfDisable && state.authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki" && !response.headersSent) {
|
||||
response.writeHead(403,"'X-Requested-With' header required to login to '" + this.servername + "'");
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
if (response.headersSent) return;
|
||||
// Receive the request body if necessary and hand off to the route handler
|
||||
if(route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") {
|
||||
// Let the route handle the request stream itself
|
||||
|
@ -0,0 +1,60 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/change-password.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
POST /change-user-password
|
||||
|
||||
\*/
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
var authenticator = require("$:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js").Authenticator;
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/change-user-password\/?$/;
|
||||
|
||||
exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
if(!state.authenticatedUser) {
|
||||
response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" });
|
||||
response.end("Unauthorized");
|
||||
return;
|
||||
}
|
||||
var auth = authenticator(state.server.sqlTiddlerDatabase);
|
||||
|
||||
var userId = state.authenticatedUser.user_id;
|
||||
var newPassword = state.data.newPassword;
|
||||
var confirmPassword = state.data.confirmPassword;
|
||||
|
||||
if(newPassword !== confirmPassword) {
|
||||
response.setHeader("Set-Cookie", "flashMessage=New passwords do not match; Path=/; HttpOnly; Max-Age=5");
|
||||
response.writeHead(302, { "Location": "/admin/users/" + userId });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
var userData = state.server.sqlTiddlerDatabase.getUser(userId);
|
||||
|
||||
if(!userData) {
|
||||
response.setHeader("Set-Cookie", "flashMessage=User not found; Path=/; HttpOnly; Max-Age=5");
|
||||
response.writeHead(302, { "Location": "/admin/users/" + userId });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
var newHash = auth.hashPassword(newPassword);
|
||||
var result = state.server.sqlTiddlerDatabase.updateUserPassword(userId, newHash);
|
||||
|
||||
response.setHeader("Set-Cookie", `flashMessage=${result.message}; Path=/; HttpOnly; Max-Age=5`);
|
||||
response.writeHead(302, { "Location": "/admin/users/" + userId });
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -0,0 +1,41 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/delete-acl.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
POST /admin/delete-acl
|
||||
|
||||
\*/
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/admin\/delete-acl\/?$/;
|
||||
|
||||
|
||||
exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
|
||||
var recipe_name = state.data.recipe_name;
|
||||
var bag_name = state.data.bag_name;
|
||||
var acl_id = state.data.acl_id;
|
||||
var entity_type = state.data.entity_type;
|
||||
|
||||
aclMiddleware(request, response, state, entity_type, "WRITE");
|
||||
|
||||
sqlTiddlerDatabase.deleteACL(acl_id);
|
||||
|
||||
response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name });
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -12,25 +12,32 @@ DELETE /bags/:bag_name/tiddler/:title
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "DELETE";
|
||||
|
||||
exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
aclMiddleware(request, response, state, "bag", "WRITE");
|
||||
// Get the parameters
|
||||
var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
title = $tw.utils.decodeURIComponentSafe(state.params[1]);
|
||||
if(bag_name) {
|
||||
var result = $tw.mws.store.deleteTiddler(title,bag_name);
|
||||
response.writeHead(204, "OK", {
|
||||
"X-Revision-Number": result.tiddler_id.toString(),
|
||||
Etag: state.makeTiddlerEtag(result),
|
||||
"Content-Type": "text/plain"
|
||||
});
|
||||
response.end();
|
||||
if(!response.headersSent) {
|
||||
var result = $tw.mws.store.deleteTiddler(title,bag_name);
|
||||
response.writeHead(204, "OK", {
|
||||
"X-Revision-Number": result.tiddler_id.toString(),
|
||||
Etag: state.makeTiddlerEtag(result),
|
||||
"Content-Type": "text/plain"
|
||||
});
|
||||
response.end();
|
||||
}
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
if(!response.headersSent) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,57 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/delete-role.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
POST /admin/delete-role
|
||||
|
||||
\*/
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/admin\/delete-role\/?$/;
|
||||
|
||||
exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
|
||||
var role_id = state.data.role_id;
|
||||
|
||||
if(!state.authenticatedUser || !state.authenticatedUser.isAdmin) {
|
||||
response.writeHead(403, "Forbidden");
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the role exists
|
||||
var role = sqlTiddlerDatabase.getRoleById(role_id);
|
||||
if(!role) {
|
||||
response.writeHead(404, "Not Found");
|
||||
response.end("Role not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the role is in use
|
||||
var isRoleInUse = sqlTiddlerDatabase.isRoleInUse(role_id);
|
||||
if(isRoleInUse) {
|
||||
response.writeHead(400, "Bad Request");
|
||||
response.end("Cannot delete role as it is still in use");
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the role
|
||||
sqlTiddlerDatabase.deleteRole(role_id);
|
||||
|
||||
// Redirect back to the roles management page
|
||||
response.writeHead(302, { "Location": "/admin/roles" });
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -0,0 +1,97 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-acl.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
GET /admin/acl
|
||||
|
||||
\*/
|
||||
(function () {
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/admin\/acl\/(.+)$/;
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
|
||||
var params = state.params[0].split("/")
|
||||
var recipeName = params[0];
|
||||
var bagName = params[params.length - 1];
|
||||
|
||||
var recipes = sqlTiddlerDatabase.listRecipes()
|
||||
var bags = sqlTiddlerDatabase.listBags()
|
||||
|
||||
var recipe = recipes.find((entry) => entry.recipe_name === recipeName && entry.bag_names.includes(bagName))
|
||||
var bag = bags.find((entry) => entry.bag_name === bagName);
|
||||
|
||||
if (!recipe || !bag) {
|
||||
response.writeHead(500, "Unable to handle request", { "Content-Type": "text/html" });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
var recipeAclRecords = sqlTiddlerDatabase.getEntityAclRecords(recipe.recipe_name);
|
||||
var bagAclRecords = sqlTiddlerDatabase.getEntityAclRecords(bag.bag_name);
|
||||
var roles = state.server.sqlTiddlerDatabase.listRoles();
|
||||
var permissions = state.server.sqlTiddlerDatabase.listPermissions();
|
||||
|
||||
// This ensures that the user attempting to view the ACL management page has permission to do so
|
||||
if(!state.authenticatedUser || (recipeAclRecords.length > 0 && !sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser.user_id, recipeName, 'WRITE'))){
|
||||
response.writeHead(403, "Forbidden");
|
||||
response.end();
|
||||
return
|
||||
}
|
||||
|
||||
// Enhance ACL records with role and permission details
|
||||
recipeAclRecords = recipeAclRecords.map(record => {
|
||||
var role = roles.find(role => role.role_id === record.role_id);
|
||||
var permission = permissions.find(perm => perm.permission_id === record.permission_id);
|
||||
return ({
|
||||
...record,
|
||||
role,
|
||||
permission,
|
||||
role_name: role.role_name,
|
||||
role_description: role.description,
|
||||
permission_name: permission.permission_name,
|
||||
permission_description: permission.description
|
||||
})
|
||||
});
|
||||
|
||||
bagAclRecords = bagAclRecords.map(record => {
|
||||
var role = roles.find(role => role.role_id === record.role_id);
|
||||
var permission = permissions.find(perm => perm.permission_id === record.permission_id);
|
||||
return ({
|
||||
...record,
|
||||
role,
|
||||
permission,
|
||||
role_name: role.role_name,
|
||||
role_description: role.description,
|
||||
permission_name: permission.permission_name,
|
||||
permission_description: permission.description
|
||||
})
|
||||
});
|
||||
|
||||
response.writeHead(200, "OK", { "Content-Type": "text/html" });
|
||||
|
||||
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", {
|
||||
variables: {
|
||||
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl",
|
||||
"roles-list": JSON.stringify(roles),
|
||||
"permissions-list": JSON.stringify(permissions),
|
||||
"bag": JSON.stringify(bag),
|
||||
"recipe": JSON.stringify(recipe),
|
||||
"recipe-acl-records": JSON.stringify(recipeAclRecords),
|
||||
"bag-acl-records": JSON.stringify(bagAclRecords),
|
||||
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
|
||||
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
|
||||
}
|
||||
});
|
||||
|
||||
response.write(html);
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -12,17 +12,20 @@ GET /bags/:bag_name/tiddler/:title/blob
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/bags\/([^\/]+)\/tiddlers\/([^\/]+)\/blob$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
aclMiddleware(request, response, state, "bag", "READ");
|
||||
// Get the parameters
|
||||
const bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
title = $tw.utils.decodeURIComponentSafe(state.params[1]);
|
||||
if(bag_name) {
|
||||
const result = $tw.mws.store.getBagTiddlerStream(title,bag_name);
|
||||
if(result) {
|
||||
if(result && !response.headersSent) {
|
||||
response.writeHead(200, "OK",{
|
||||
Etag: state.makeTiddlerEtag(result),
|
||||
"Content-Type": result.type,
|
||||
@ -31,8 +34,10 @@ exports.handler = function(request,response,state) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
if (!response.headersSent) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
||||
|
@ -16,11 +16,14 @@ fallback=<url> // Optional redirect if the tiddler is not found
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/bags\/([^\/]+)\/tiddlers\/(.+)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
aclMiddleware(request, response, state, "bag", "READ");
|
||||
// Get the parameters
|
||||
const bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
title = $tw.utils.decodeURIComponentSafe(state.params[1]),
|
||||
@ -37,29 +40,37 @@ exports.handler = function(request,response,state) {
|
||||
// This is not a JSON API request, we should return the raw tiddler content
|
||||
const result = $tw.mws.store.getBagTiddlerStream(title,bag_name);
|
||||
if(result) {
|
||||
response.writeHead(200, "OK",{
|
||||
Etag: state.makeTiddlerEtag(result),
|
||||
"Content-Type": result.type
|
||||
});
|
||||
if(!response.headersSent){
|
||||
response.writeHead(200, "OK",{
|
||||
Etag: state.makeTiddlerEtag(result),
|
||||
"Content-Type": result.type
|
||||
});
|
||||
}
|
||||
result.stream.pipe(response);
|
||||
return;
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
if(!response.headersSent){
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Redirect to fallback URL if tiddler not found
|
||||
if(state.queryParameters.fallback) {
|
||||
response.writeHead(302, "OK",{
|
||||
"Location": state.queryParameters.fallback
|
||||
});
|
||||
response.end();
|
||||
if (!response.headersSent){
|
||||
response.writeHead(302, "OK",{
|
||||
"Location": state.queryParameters.fallback
|
||||
});
|
||||
response.end();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
if(!response.headersSent){
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ GET /bags/:bag_name/
|
||||
GET /bags/:bag_name
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
@ -17,38 +17,46 @@ exports.method = "GET";
|
||||
|
||||
exports.path = /^\/bags\/([^\/]+)(\/?)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "bag"
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
// Redirect if there is no trailing slash. We do this so that the relative URL specified in the upload form works correctly
|
||||
if(state.params[1] !== "/") {
|
||||
state.redirect(301,state.urlInfo.path + "/");
|
||||
if (state.params[1] !== "/") {
|
||||
state.redirect(301, state.urlInfo.path + "/");
|
||||
return;
|
||||
}
|
||||
// Get the parameters
|
||||
var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
bagTiddlers = bag_name && $tw.mws.store.getBagTiddlers(bag_name);
|
||||
if(bag_name && bagTiddlers) {
|
||||
if (bag_name && bagTiddlers) {
|
||||
// If application/json is requested then this is an API request, and gets the response in JSON
|
||||
if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
|
||||
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(bagTiddlers),"utf8");
|
||||
if (request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
|
||||
state.sendResponse(200, { "Content-Type": "application/json" }, JSON.stringify(bagTiddlers), "utf8");
|
||||
} else {
|
||||
// This is not a JSON API request, we should return the raw tiddler content
|
||||
response.writeHead(200, "OK",{
|
||||
"Content-Type": "text/html"
|
||||
});
|
||||
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain","$:/plugins/tiddlywiki/multiwikiserver/templates/page",{
|
||||
variables: {
|
||||
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-bag",
|
||||
"bag-name": bag_name,
|
||||
"bag-titles": JSON.stringify(bagTiddlers.map(bagTiddler => bagTiddler.title)),
|
||||
"bag-tiddlers": JSON.stringify(bagTiddlers)
|
||||
}
|
||||
});
|
||||
response.write(html);
|
||||
response.end();
|
||||
if (!response.headersSent) {
|
||||
// This is not a JSON API request, we should return the raw tiddler content
|
||||
response.writeHead(200, "OK", {
|
||||
"Content-Type": "text/html"
|
||||
});
|
||||
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", {
|
||||
variables: {
|
||||
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-bag",
|
||||
"bag-name": bag_name,
|
||||
"bag-titles": JSON.stringify(bagTiddlers.map(bagTiddler => bagTiddler.title)),
|
||||
"bag-tiddlers": JSON.stringify(bagTiddlers)
|
||||
}
|
||||
});
|
||||
response.write(html);
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
if (!response.headersSent) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -19,7 +19,9 @@ exports.path = /^\/$/;
|
||||
exports.handler = function(request,response,state) {
|
||||
// Get the bag and recipe information
|
||||
var bagList = $tw.mws.store.listBags(),
|
||||
recipeList = $tw.mws.store.listRecipes();
|
||||
recipeList = $tw.mws.store.listRecipes(),
|
||||
sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
|
||||
|
||||
// If application/json is requested then this is an API request, and gets the response in JSON
|
||||
if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
|
||||
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipes),"utf8");
|
||||
@ -28,13 +30,19 @@ exports.handler = function(request,response,state) {
|
||||
response.writeHead(200, "OK",{
|
||||
"Content-Type": "text/html"
|
||||
});
|
||||
// filter bags and recipies by user's read access from ACL
|
||||
var allowedRecipes = recipeList.filter(recipe => sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, recipe.recipe_name, 'READ'));
|
||||
var allowedBags = bagList.filter(bag => sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, bag.bag_name, 'READ'));
|
||||
|
||||
// Render the html
|
||||
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain","$:/plugins/tiddlywiki/multiwikiserver/templates/page",{
|
||||
variables: {
|
||||
"show-system": state.queryParameters.show_system || "off",
|
||||
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-index",
|
||||
"bag-list": JSON.stringify(bagList),
|
||||
"recipe-list": JSON.stringify(recipeList)
|
||||
"bag-list": JSON.stringify(allowedBags),
|
||||
"recipe-list": JSON.stringify(allowedRecipes),
|
||||
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
|
||||
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
|
||||
}
|
||||
});
|
||||
response.write(html);
|
||||
|
@ -0,0 +1,39 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-login.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
GET /login
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/login$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
// Check if the user already has a valid session
|
||||
var authenticatedUser = state.server.authenticateUser(request, response);
|
||||
if(authenticatedUser) {
|
||||
// User is already logged in, redirect to home page
|
||||
response.writeHead(302, { "Location": "/" });
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
var loginTiddler = $tw.mws.store.adminWiki.getTiddler("$:/plugins/tiddlywiki/multiwikiserver/auth/form/login");
|
||||
if(loginTiddler) {
|
||||
var text = $tw.mws.store.adminWiki.renderTiddler("text/html", loginTiddler.fields.title);
|
||||
response.writeHead(200, { "Content-Type": "text/html" });
|
||||
response.end(text);
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end("Login page not found");
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
@ -20,6 +20,10 @@ exports.method = "GET";
|
||||
|
||||
exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "recipe"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
// Get the parameters
|
||||
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
@ -38,27 +42,32 @@ exports.handler = function(request,response,state) {
|
||||
} else {
|
||||
// This is not a JSON API request, we should return the raw tiddler content
|
||||
var type = tiddlerInfo.tiddler.type || "text/plain";
|
||||
response.writeHead(200, "OK",{
|
||||
if(!response.headersSent) {
|
||||
response.writeHead(200, "OK",{
|
||||
Etag: state.makeTiddlerEtag(tiddlerInfo),
|
||||
"Content-Type": type
|
||||
});
|
||||
response.write(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding);
|
||||
response.end();;
|
||||
"Content-Type": type
|
||||
});
|
||||
response.write(tiddlerInfo.tiddler.text || "",($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding);
|
||||
response.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Redirect to fallback URL if tiddler not found
|
||||
if(state.queryParameters.fallback) {
|
||||
response.writeHead(302, "OK",{
|
||||
"Location": state.queryParameters.fallback
|
||||
});
|
||||
response.end();
|
||||
return;
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
return;
|
||||
if(!response.headersSent) {
|
||||
// Redirect to fallback URL if tiddler not found
|
||||
if(state.queryParameters.fallback) {
|
||||
response.writeHead(302, "OK",{
|
||||
"Location": state.queryParameters.fallback
|
||||
});
|
||||
response.end();
|
||||
return;
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -17,22 +17,24 @@ exports.method = "GET";
|
||||
exports.path = /^\/recipes\/([^\/]+)\/tiddlers.json$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
// Get the parameters
|
||||
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]);
|
||||
if(recipe_name) {
|
||||
// Get the tiddlers in the recipe, optionally since the specified last known tiddler_id
|
||||
var recipeTiddlers = $tw.mws.store.getRecipeTiddlers(recipe_name,{
|
||||
include_deleted: state.queryParameters.include_deleted === "true",
|
||||
last_known_tiddler_id: state.queryParameters.last_known_tiddler_id
|
||||
});
|
||||
if(recipeTiddlers) {
|
||||
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipeTiddlers),"utf8");
|
||||
return;
|
||||
if(!response.headersSent) {
|
||||
// Get the parameters
|
||||
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]);
|
||||
if(recipe_name) {
|
||||
// Get the tiddlers in the recipe, optionally since the specified last known tiddler_id
|
||||
var recipeTiddlers = $tw.mws.store.getRecipeTiddlers(recipe_name,{
|
||||
include_deleted: state.queryParameters.include_deleted === "true",
|
||||
last_known_tiddler_id: state.queryParameters.last_known_tiddler_id
|
||||
});
|
||||
if(recipeTiddlers) {
|
||||
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(recipeTiddlers),"utf8");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fail if something went wrong
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
// Fail if something went wrong
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,54 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-users.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
GET /admin/users
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/admin\/users$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
var userList = state.server.sqlTiddlerDatabase.listUsers();
|
||||
|
||||
// Ensure userList is an array
|
||||
if (!Array.isArray(userList)) {
|
||||
userList = [];
|
||||
console.error("userList is not an array");
|
||||
}
|
||||
|
||||
// Convert dates to strings and ensure all necessary fields are present
|
||||
userList = userList.map(user => ({
|
||||
user_id: user.user_id || '',
|
||||
username: user.username || '',
|
||||
email: user.email || '',
|
||||
created_at: user.created_at ? new Date(user.created_at).toISOString() : '',
|
||||
last_login: user.last_login ? new Date(user.last_login).toISOString() : ''
|
||||
}));
|
||||
|
||||
response.writeHead(200, "OK", {
|
||||
"Content-Type": "text/html"
|
||||
});
|
||||
|
||||
// Render the html
|
||||
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain","$:/plugins/tiddlywiki/multiwikiserver/templates/page",{
|
||||
variables: {
|
||||
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-users",
|
||||
"user-list": JSON.stringify(userList),
|
||||
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
|
||||
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
|
||||
}
|
||||
});
|
||||
response.write(html);
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -16,6 +16,10 @@ exports.method = "GET";
|
||||
|
||||
exports.path = /^\/wiki\/([^\/]+)$/;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "recipe"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
// Get the recipe name from the parameters
|
||||
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
|
@ -0,0 +1,36 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/manage-roles.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
GET /admin/manage-roles
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/admin\/roles\/?$/;
|
||||
|
||||
exports.handler = function(request, response, state) {
|
||||
var roles = state.server.sqlTiddlerDatabase.listRoles();
|
||||
|
||||
response.writeHead(200, "OK", {"Content-Type": "text/html"});
|
||||
|
||||
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", {
|
||||
variables: {
|
||||
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-roles",
|
||||
"roles-list": JSON.stringify(roles),
|
||||
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
|
||||
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
|
||||
}
|
||||
});
|
||||
response.write(html);
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -0,0 +1,68 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/manage-user.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
GET /admin/users/:user_id
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/admin\/users\/([^\/]+)\/?$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
var user_id = $tw.utils.decodeURIComponentSafe(state.params[0]);
|
||||
var userData = state.server.sqlTiddlerDatabase.getUser(user_id);
|
||||
|
||||
if(!userData) {
|
||||
response.writeHead(404, "Not Found", {"Content-Type": "text/html"});
|
||||
var errorHtml = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/error", {
|
||||
variables: {
|
||||
"error-message": "User not found"
|
||||
}
|
||||
});
|
||||
response.write(errorHtml);
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert dates to strings and ensure all necessary fields are present
|
||||
const user = {
|
||||
user_id: userData.user_id || '',
|
||||
username: userData.username || '',
|
||||
email: userData.email || '',
|
||||
created_at: userData.created_at ? new Date(userData.created_at).toISOString() : '',
|
||||
last_login: userData.last_login ? new Date(userData.last_login).toISOString() : ''
|
||||
};
|
||||
|
||||
// Get all roles which the user has been assigned
|
||||
var userRole = state.server.sqlTiddlerDatabase.getUserRoles(user_id);
|
||||
var allRoles = state.server.sqlTiddlerDatabase.listRoles();
|
||||
|
||||
response.writeHead(200, "OK", {
|
||||
"Content-Type": "text/html"
|
||||
});
|
||||
|
||||
// Render the html
|
||||
var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", {
|
||||
variables: {
|
||||
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/manage-user",
|
||||
"user": JSON.stringify(user),
|
||||
"user-role": JSON.stringify(userRole),
|
||||
"all-roles": JSON.stringify(allRoles),
|
||||
"is-current-user-profile": state.authenticatedUser && state.authenticatedUser.user_id === $tw.utils.parseInt(user_id, 10) ? "yes" : "no",
|
||||
"username": state.authenticatedUser ? state.authenticatedUser.username : "Guest",
|
||||
"user-is-admin": state.authenticatedUser && state.authenticatedUser.isAdmin ? "yes" : "no"
|
||||
}
|
||||
});
|
||||
response.write(html);
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -0,0 +1,64 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-acl.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
POST /admin/post-acl
|
||||
|
||||
\*/
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/admin\/post-acl\/?$/;
|
||||
|
||||
|
||||
exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
|
||||
var entity_type = state.data.entity_type;
|
||||
var recipe_name = state.data.recipe_name;
|
||||
var bag_name = state.data.bag_name;
|
||||
var role_id = state.data.role_id;
|
||||
var permission_id = state.data.permission_id;
|
||||
var isRecipe = entity_type === "recipe"
|
||||
|
||||
var entityAclRecords = sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true);
|
||||
|
||||
var aclExists = entityAclRecords.some((record) => (
|
||||
record.role_id == role_id && record.permission_id == permission_id
|
||||
))
|
||||
|
||||
// This ensures that the user attempting to modify the ACL has permission to do so
|
||||
// if(!state.authenticatedUser || (entityAclRecords.length > 0 && !sqlTiddlerDatabase[isRecipe ? 'hasRecipePermission' : 'hasBagPermission'](state.authenticatedUser.user_id, isRecipe ? recipe_name : bag_name, 'WRITE'))){
|
||||
// response.writeHead(403, "Forbidden");
|
||||
// response.end();
|
||||
// return
|
||||
// }
|
||||
|
||||
if (aclExists) {
|
||||
// do nothing, return the user back to the form
|
||||
response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name });
|
||||
response.end();
|
||||
return
|
||||
}
|
||||
|
||||
sqlTiddlerDatabase.createACL(
|
||||
isRecipe ? recipe_name : bag_name,
|
||||
entity_type,
|
||||
role_id,
|
||||
permission_id
|
||||
)
|
||||
|
||||
response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name });
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -20,6 +20,10 @@ exports.bodyFormat = "stream";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "bag"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
const path = require("path"),
|
||||
fs = require("fs"),
|
||||
@ -39,29 +43,31 @@ exports.handler = function(request,response,state) {
|
||||
"imported-tiddlers": results
|
||||
}));
|
||||
} else {
|
||||
response.writeHead(200, "OK",{
|
||||
"Content-Type": "text/html"
|
||||
});
|
||||
response.write(`
|
||||
<!doctype html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
`);
|
||||
// Render the html
|
||||
var html = $tw.mws.store.adminWiki.renderTiddler("text/html","$:/plugins/tiddlywiki/multiwikiserver/templates/post-bag-tiddlers",{
|
||||
variables: {
|
||||
"bag-name": bag_name,
|
||||
"imported-titles": JSON.stringify(results)
|
||||
}
|
||||
});
|
||||
response.write(html);
|
||||
response.write(`
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
response.end();
|
||||
if(!response.headersSent) {
|
||||
response.writeHead(200, "OK",{
|
||||
"Content-Type": "text/html"
|
||||
});
|
||||
response.write(`
|
||||
<!doctype html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
`);
|
||||
// Render the html
|
||||
var html = $tw.mws.store.adminWiki.renderTiddler("text/html","$:/plugins/tiddlywiki/multiwikiserver/templates/post-bag-tiddlers",{
|
||||
variables: {
|
||||
"bag-name": bag_name,
|
||||
"imported-titles": JSON.stringify(results)
|
||||
}
|
||||
});
|
||||
response.write(html);
|
||||
response.write(`
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -25,6 +25,10 @@ exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "bag"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
if(state.data.bag_name) {
|
||||
const result = $tw.mws.store.createBag(state.data.bag_name,state.data.description);
|
||||
|
@ -0,0 +1,67 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-login.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
POST /login
|
||||
|
||||
Parameters:
|
||||
|
||||
username
|
||||
password
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
var authenticator = require('$:/plugins/tiddlywiki/multiwikiserver/auth/authentication.js').Authenticator;
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/login$/;
|
||||
|
||||
exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
var auth = authenticator(state.server.sqlTiddlerDatabase);
|
||||
var username = state.data.username;
|
||||
var password = state.data.password;
|
||||
var user = state.server.sqlTiddlerDatabase.getUserByUsername(username);
|
||||
var isPasswordValid = auth.verifyPassword(password, user ? user.password : null)
|
||||
|
||||
if(user && isPasswordValid) {
|
||||
var sessionId = auth.createSession(user.user_id);
|
||||
var returnUrl = state.server.parseCookieString(request.headers.cookie).returnUrl
|
||||
response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
|
||||
if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
|
||||
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify({
|
||||
"sessionId": sessionId
|
||||
}));
|
||||
} else {
|
||||
response.writeHead(302, {
|
||||
'Location': returnUrl || '/'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
|
||||
title: "$:/temp/mws/login/error",
|
||||
text: "Invalid username or password"
|
||||
}));
|
||||
if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
|
||||
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify({
|
||||
"message": "Invalid username or password"
|
||||
}));
|
||||
} else {
|
||||
response.writeHead(302, {
|
||||
'Location': '/login'
|
||||
});
|
||||
}
|
||||
}
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -0,0 +1,37 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-logout.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
POST /logout
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/logout$/;
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
// if(state.authenticatedUser) {
|
||||
state.server.sqlTiddlerDatabase.deleteSession(state.authenticatedUser.sessionId);
|
||||
// }
|
||||
var cookies = request.headers.cookie ? request.headers.cookie.split(";") : [];
|
||||
for(var i = 0; i < cookies.length; i++) {
|
||||
var cookie = cookies[i].trim().split("=")[0];
|
||||
response.setHeader("Set-Cookie", cookie + "=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict");
|
||||
}
|
||||
|
||||
// response.setHeader("Set-Cookie", "session=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
||||
// response.setHeader("Set-Cookie", "returnUrl=; HttpOnly; Path=/");
|
||||
response.writeHead(302, { "Location": "/login" });
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -26,6 +26,10 @@ exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "recipe"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
if(state.data.recipe_name && state.data.bag_names) {
|
||||
const result = $tw.mws.store.createRecipe(state.data.recipe_name,$tw.utils.parseStringArray(state.data.bag_names),state.data.description);
|
||||
|
@ -0,0 +1,36 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-role.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
POST /admin/post-role
|
||||
|
||||
\*/
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/admin\/post-role\/?$/;
|
||||
|
||||
exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
|
||||
var role_name = state.data.role_name;
|
||||
var role_description = state.data.role_description;
|
||||
|
||||
// Add your authentication check here if needed
|
||||
|
||||
sqlTiddlerDatabase.createRole(role_name, role_description);
|
||||
|
||||
response.writeHead(302, { "Location": "/admin/roles" });
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -0,0 +1,63 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-user.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
POST /admin/post-user
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/admin\/post-user\/?$/;
|
||||
|
||||
exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.handler = function(request, response, state) {
|
||||
var sqlTiddlerDatabase = state.server.sqlTiddlerDatabase;
|
||||
var username = state.data.username;
|
||||
var email = state.data.email;
|
||||
var password = state.data.password;
|
||||
var confirmPassword = state.data.confirmPassword;
|
||||
|
||||
if(!state.authenticatedUser) {
|
||||
response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" });
|
||||
response.end("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!username || !email || !password || !confirmPassword) {
|
||||
response.writeHead(400, {"Content-Type": "application/json"});
|
||||
response.end(JSON.stringify({error: "All fields are required"}));
|
||||
return;
|
||||
}
|
||||
|
||||
if(password !== confirmPassword) {
|
||||
response.writeHead(400, {"Content-Type": "application/json"});
|
||||
response.end(JSON.stringify({error: "Passwords do not match"}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
var existingUser = sqlTiddlerDatabase.getUser(username);
|
||||
if(existingUser) {
|
||||
response.writeHead(400, {"Content-Type": "application/json"});
|
||||
response.end(JSON.stringify({error: "Username already exists"}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new user
|
||||
var userId = sqlTiddlerDatabase.createUser(username, email, password);
|
||||
|
||||
response.writeHead(302, {"Location": "/admin/users/"+userId});
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -16,12 +16,16 @@ exports.method = "PUT";
|
||||
|
||||
exports.path = /^\/bags\/(.+)$/;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "bag"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
// Get the parameters
|
||||
var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
data = $tw.utils.parseJSONSafe(state.data);
|
||||
if(bag_name && data) {
|
||||
const result = $tw.mws.store.createBag(bag_name,data.description);
|
||||
var result = $tw.mws.store.createBag(bag_name,data.description);
|
||||
if(!result) {
|
||||
state.sendResponse(204,{
|
||||
"Content-Type": "text/plain"
|
||||
@ -34,8 +38,10 @@ exports.handler = function(request,response,state) {
|
||||
"utf8");
|
||||
}
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
if(!response.headersSent) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -6,7 +6,7 @@ module-type: mws-route
|
||||
PUT /recipes/:recipe_name/tiddlers/:title
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
@ -16,30 +16,37 @@ exports.method = "PUT";
|
||||
|
||||
exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "recipe"
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
// Get the parameters
|
||||
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
title = $tw.utils.decodeURIComponentSafe(state.params[1]),
|
||||
fields = $tw.utils.parseJSONSafe(state.data);
|
||||
if(recipe_name && title === fields.title) {
|
||||
var result = $tw.mws.store.saveRecipeTiddler(fields,recipe_name);
|
||||
if(result) {
|
||||
response.writeHead(204, "OK",{
|
||||
"X-Revision-Number": result.tiddler_id.toString(),
|
||||
"X-Bag-Name": result.bag_name,
|
||||
Etag: state.makeTiddlerEtag(result),
|
||||
"Content-Type": "text/plain"
|
||||
});
|
||||
} else {
|
||||
response.writeHead(400);
|
||||
var result = $tw.mws.store.saveRecipeTiddler(fields, recipe_name);
|
||||
if(!response.headersSent) {
|
||||
if(result) {
|
||||
response.writeHead(204, "OK", {
|
||||
"X-Revision-Number": result.tiddler_id.toString(),
|
||||
"X-Bag-Name": result.bag_name,
|
||||
Etag: state.makeTiddlerEtag(result),
|
||||
"Content-Type": "text/plain"
|
||||
});
|
||||
} else {
|
||||
response.writeHead(400);
|
||||
}
|
||||
response.end();
|
||||
}
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
// Fail if something went wrong
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
|
||||
if(!response.headersSent) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
||||
|
@ -6,7 +6,7 @@ module-type: mws-route
|
||||
PUT /recipes/:recipe_name
|
||||
|
||||
\*/
|
||||
(function() {
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
@ -16,26 +16,32 @@ exports.method = "PUT";
|
||||
|
||||
exports.path = /^\/recipes\/(.+)$/;
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "recipe"
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
// Get the parameters
|
||||
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
data = $tw.utils.parseJSONSafe(state.data);
|
||||
if(recipe_name && data) {
|
||||
const result = $tw.mws.store.createRecipe(recipe_name,data.bag_names,data.description);
|
||||
var result = $tw.mws.store.createRecipe(recipe_name, data.bag_names, data.description);
|
||||
if(!result) {
|
||||
state.sendResponse(204,{
|
||||
state.sendResponse(204, {
|
||||
"Content-Type": "text/plain"
|
||||
});
|
||||
} else {
|
||||
state.sendResponse(400,{
|
||||
state.sendResponse(400, {
|
||||
"Content-Type": "text/plain"
|
||||
},
|
||||
result.message,
|
||||
"utf8");
|
||||
result.message,
|
||||
"utf8");
|
||||
}
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
if(!response.headersSent) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,47 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/update-profile.js
|
||||
type: application/javascript
|
||||
module-type: mws-route
|
||||
|
||||
POST /update-user-profile
|
||||
|
||||
\*/
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/update-user-profile\/?$/;
|
||||
|
||||
exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.handler = function (request,response,state) {
|
||||
if(!state.authenticatedUser) {
|
||||
response.writeHead(401, "Unauthorized", { "Content-Type": "text/plain" });
|
||||
response.end("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = state.authenticatedUser.user_id;
|
||||
var username = state.data.username;
|
||||
var email = state.data.email;
|
||||
var roleId = state.data.role;
|
||||
|
||||
var result = state.server.sqlTiddlerDatabase.updateUser(userId, username, email, roleId);
|
||||
|
||||
if(result.success) {
|
||||
response.setHeader("Set-Cookie", "flashMessage="+result.message+"; Path=/; HttpOnly; Max-Age=5");
|
||||
response.writeHead(302, { "Location": "/admin/users/" + userId });
|
||||
} else {
|
||||
response.setHeader("Set-Cookie", "flashMessage="+result.message+"; Path=/; HttpOnly; Max-Age=5");
|
||||
response.writeHead(302, { "Location": "/admin/users/" + userId });
|
||||
}
|
||||
response.end();
|
||||
};
|
||||
|
||||
}());
|
@ -0,0 +1,79 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js
|
||||
type: application/javascript
|
||||
module-type: library
|
||||
|
||||
Middleware to handle ACL permissions
|
||||
|
||||
\*/
|
||||
|
||||
(function () {
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
ACL Middleware factory function
|
||||
*/
|
||||
function redirectToLogin(response, returnUrl) {
|
||||
if(!response.headersSent) {
|
||||
var validReturnUrlRegex = /^\/(?!.*\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2|ttf|eot|json)$).*$/;
|
||||
var sanitizedReturnUrl = '/'; // Default to home page
|
||||
|
||||
if(validReturnUrlRegex.test(returnUrl)) {
|
||||
sanitizedReturnUrl = returnUrl;
|
||||
response.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Secure; SameSite=Strict; Path=/`);
|
||||
} else{
|
||||
console.log(`Invalid return URL detected: ${returnUrl}. Redirecting to home page.`);
|
||||
}
|
||||
const loginUrl = '/login';
|
||||
response.writeHead(302, {
|
||||
'Location': loginUrl
|
||||
});
|
||||
response.end();
|
||||
}
|
||||
};
|
||||
|
||||
exports.middleware = function (request, response, state, entityType, permissionName) {
|
||||
|
||||
var server = state.server,
|
||||
sqlTiddlerDatabase = server.sqlTiddlerDatabase,
|
||||
entityName = state.data ? (state.data[entityType+"_name"] || state.params[0]) : state.params[0];
|
||||
|
||||
// First, replace '%3A' with ':' to handle TiddlyWiki's system tiddlers
|
||||
var partiallyDecoded = entityName.replace(/%3A/g, ":");
|
||||
// Then use decodeURIComponent for the rest
|
||||
var decodedEntityName = decodeURIComponent(partiallyDecoded);
|
||||
var aclRecord = sqlTiddlerDatabase.getACLByName(entityType, decodedEntityName);
|
||||
// Get permission record
|
||||
const permission = sqlTiddlerDatabase.getPermissionByName(permissionName);
|
||||
// ACL Middleware will only apply if the entity has a middleware record
|
||||
if(aclRecord && aclRecord?.permission_id === permission?.permission_id) {
|
||||
// If not authenticated and anonymous access is not allowed, request authentication
|
||||
if(!state.authenticatedUsername && !state.allowAnon) {
|
||||
if(state.urlInfo.pathname !== '/login') {
|
||||
redirectToLogin(response, request.url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check if user is authenticated
|
||||
if(!state.authenticatedUser && !response.headersSent) {
|
||||
response.writeHead(401, "Unauthorized");
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check ACL permission
|
||||
var hasPermission = request.method === "POST" || sqlTiddlerDatabase.checkACLPermission(state.authenticatedUser.user_id, entityType, decodedEntityName, permissionName)
|
||||
if(!hasPermission) {
|
||||
if(!response.headersSent) {
|
||||
response.writeHead(403, "Forbidden");
|
||||
response.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
@ -25,6 +25,16 @@ function SqlTiddlerDatabase(options) {
|
||||
databasePath: options.databasePath,
|
||||
engine: options.engine
|
||||
});
|
||||
this.entityTypeToTableMap = {
|
||||
bag: {
|
||||
table: "bags",
|
||||
column: "bag_name"
|
||||
},
|
||||
recipe: {
|
||||
table: "recipes",
|
||||
column: "recipe_name"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SqlTiddlerDatabase.prototype.close = function() {
|
||||
@ -38,6 +48,83 @@ SqlTiddlerDatabase.prototype.transaction = function(fn) {
|
||||
|
||||
SqlTiddlerDatabase.prototype.createTables = function() {
|
||||
this.engine.runStatements([`
|
||||
-- Users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
last_login TEXT
|
||||
)
|
||||
`,`
|
||||
-- User Session table
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
user_id INTEGER NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
last_accessed TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(user_id)
|
||||
)
|
||||
`,`
|
||||
-- Groups table
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
group_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
group_name TEXT UNIQUE NOT NULL,
|
||||
description TEXT
|
||||
)
|
||||
`,`
|
||||
-- Roles table
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
role_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role_name TEXT UNIQUE NOT NULL,
|
||||
description TEXT
|
||||
)
|
||||
`,`
|
||||
-- Permissions table
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
permission_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
permission_name TEXT UNIQUE NOT NULL,
|
||||
description TEXT
|
||||
)
|
||||
`,`
|
||||
-- User-Group association table
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
user_id INTEGER,
|
||||
group_id INTEGER,
|
||||
PRIMARY KEY (user_id, group_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(user_id),
|
||||
FOREIGN KEY (group_id) REFERENCES groups(group_id)
|
||||
)
|
||||
`,`
|
||||
-- User-Role association table
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id INTEGER,
|
||||
role_id INTEGER,
|
||||
PRIMARY KEY (user_id, role_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(user_id),
|
||||
FOREIGN KEY (role_id) REFERENCES roles(role_id)
|
||||
)
|
||||
`,`
|
||||
-- Group-Role association table
|
||||
CREATE TABLE IF NOT EXISTS group_roles (
|
||||
group_id INTEGER,
|
||||
role_id INTEGER,
|
||||
PRIMARY KEY (group_id, role_id),
|
||||
FOREIGN KEY (group_id) REFERENCES groups(group_id),
|
||||
FOREIGN KEY (role_id) REFERENCES roles(role_id)
|
||||
)
|
||||
`,`
|
||||
-- Role-Permission association table
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
role_id INTEGER,
|
||||
permission_id INTEGER,
|
||||
PRIMARY KEY (role_id, permission_id),
|
||||
FOREIGN KEY (role_id) REFERENCES roles(role_id),
|
||||
FOREIGN KEY (permission_id) REFERENCES permissions(permission_id)
|
||||
)
|
||||
`,`
|
||||
-- Bags have names and access control settings
|
||||
CREATE TABLE IF NOT EXISTS bags (
|
||||
bag_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@ -82,6 +169,26 @@ SqlTiddlerDatabase.prototype.createTables = function() {
|
||||
FOREIGN KEY (tiddler_id) REFERENCES tiddlers(tiddler_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
UNIQUE (tiddler_id, field_name)
|
||||
)
|
||||
`,`
|
||||
-- ACL table (using bag/recipe ids directly)
|
||||
CREATE TABLE IF NOT EXISTS acl (
|
||||
acl_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_name TEXT NOT NULL,
|
||||
entity_type TEXT NOT NULL CHECK (entity_type IN ('bag', 'recipe')),
|
||||
role_id INTEGER,
|
||||
permission_id INTEGER,
|
||||
FOREIGN KEY (role_id) REFERENCES roles(role_id),
|
||||
FOREIGN KEY (permission_id) REFERENCES permissions(permission_id)
|
||||
)
|
||||
`,`
|
||||
-- Indexes for performance (we can add more as needed based on query patterns)
|
||||
CREATE INDEX IF NOT EXISTS idx_tiddlers_bag_id ON tiddlers(bag_id)
|
||||
`,`
|
||||
CREATE INDEX IF NOT EXISTS idx_fields_tiddler_id ON fields(tiddler_id)
|
||||
`,`
|
||||
CREATE INDEX IF NOT EXISTS idx_recipe_bags_recipe_id ON recipe_bags(recipe_id)
|
||||
`,`
|
||||
CREATE INDEX IF NOT EXISTS idx_acl_entity_id ON acl(entity_name)
|
||||
`]);
|
||||
};
|
||||
|
||||
@ -101,7 +208,7 @@ Returns the bag_id of the bag
|
||||
SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscontrol) {
|
||||
accesscontrol = accesscontrol || "";
|
||||
// Run the queries
|
||||
this.engine.runStatement(`
|
||||
var bag = this.engine.runStatement(`
|
||||
INSERT OR IGNORE INTO bags (bag_name, accesscontrol, description)
|
||||
VALUES ($bag_name, '', '')
|
||||
`,{
|
||||
@ -117,6 +224,14 @@ SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscon
|
||||
$accesscontrol: accesscontrol,
|
||||
$description: description
|
||||
});
|
||||
|
||||
const admin = this.getRoleByName("ADMIN");
|
||||
if(admin) {
|
||||
const readPermission = this.getPermissionByName("READ");
|
||||
const writePermission = this.getPermissionByName("WRITE");
|
||||
// this.createACL(bag_name, "bag", admin.role_id, readPermission.permission_id);
|
||||
// this.createACL(bag_name, "bag", admin.role_id, writePermission.permission_id);
|
||||
}
|
||||
return updateBags.lastInsertRowid;
|
||||
};
|
||||
|
||||
@ -180,6 +295,16 @@ SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,descr
|
||||
$recipe_name: recipe_name,
|
||||
$bag_names: JSON.stringify(bag_names)
|
||||
});
|
||||
|
||||
|
||||
// update the permissions on ACL records
|
||||
const admin = this.getRoleByName("ADMIN");
|
||||
if(admin) {
|
||||
const readPermission = this.getPermissionByName("READ");
|
||||
const writePermission = this.getPermissionByName("WRITE");
|
||||
// this.createACL(recipe_name, "recipe", admin.role_id, readPermission.permission_id);
|
||||
// this.createACL(recipe_name, "recipe", admin.role_id, writePermission.permission_id);
|
||||
}
|
||||
return updateRecipes.lastInsertRowid;
|
||||
};
|
||||
|
||||
@ -374,6 +499,102 @@ SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) {
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
Checks if a user has permission to access a recipe
|
||||
*/
|
||||
SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, permissionName) {
|
||||
return this.checkACLPermission(userId, "recipe", recipeName, permissionName)
|
||||
};
|
||||
|
||||
/*
|
||||
Checks if a user has permission to access a bag
|
||||
*/
|
||||
SqlTiddlerDatabase.prototype.hasBagPermission = function(userId, bagName, permissionName) {
|
||||
return this.checkACLPermission(userId, "bag", bagName, permissionName)
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fetchAll) {
|
||||
const entityInfo = this.entityTypeToTableMap[entityType];
|
||||
if (!entityInfo) {
|
||||
throw new Error("Invalid entity type: " + entityType);
|
||||
}
|
||||
|
||||
// First, check if there's an ACL record for the entity and get the permission_id
|
||||
var checkACLExistsQuery = `
|
||||
SELECT *
|
||||
FROM acl
|
||||
WHERE entity_type = $entity_type
|
||||
AND entity_name = $entity_name
|
||||
`;
|
||||
|
||||
if (!fetchAll) {
|
||||
checkACLExistsQuery += ' LIMIT 1'
|
||||
}
|
||||
|
||||
const aclRecord = this.engine[fetchAll ? 'runStatementGetAll' : 'runStatementGet'](checkACLExistsQuery, {
|
||||
$entity_type: entityType,
|
||||
$entity_name: entityName
|
||||
});
|
||||
|
||||
return aclRecord;
|
||||
}
|
||||
|
||||
SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName) {
|
||||
// if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission
|
||||
if(entityName.startsWith("$:/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const aclRecord = this.getACLByName(entityType, entityName);
|
||||
|
||||
// If no ACL record exists, return true for hasPermission
|
||||
if (!aclRecord) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If ACL record exists, check for user permission using the retrieved permission_id
|
||||
const checkPermissionQuery = `
|
||||
SELECT 1
|
||||
FROM users u
|
||||
JOIN user_roles ur ON u.user_id = ur.user_id
|
||||
JOIN roles r ON ur.role_id = r.role_id
|
||||
JOIN acl a ON r.role_id = a.role_id
|
||||
WHERE u.user_id = $user_id
|
||||
AND a.entity_type = $entity_type
|
||||
AND a.entity_name = $entity_name
|
||||
AND a.permission_id = $permission_id
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const result = this.engine.runStatementGet(checkPermissionQuery, {
|
||||
$user_id: userId,
|
||||
$entity_type: entityType,
|
||||
$entity_name: entityName,
|
||||
$permission_id: aclRecord.permission_id
|
||||
});
|
||||
|
||||
const hasPermission = result !== undefined;
|
||||
|
||||
return hasPermission;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the ACL records for an entity (bag or recipe)
|
||||
*/
|
||||
SqlTiddlerDatabase.prototype.getEntityAclRecords = function(entityName) {
|
||||
const checkACLExistsQuery = `
|
||||
SELECT *
|
||||
FROM acl
|
||||
WHERE entity_name = $entity_name
|
||||
`;
|
||||
|
||||
const aclRecords = this.engine.runStatementGetAll(checkACLExistsQuery, {
|
||||
$entity_name: entityName
|
||||
});
|
||||
|
||||
return aclRecords
|
||||
}
|
||||
|
||||
/*
|
||||
Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist
|
||||
*/
|
||||
@ -575,6 +796,576 @@ SqlTiddlerDatabase.prototype.getRecipeTiddlerAttachmentBlob = function(title,rec
|
||||
return row ? row.attachment_blob : null;
|
||||
};
|
||||
|
||||
// User CRUD operations
|
||||
SqlTiddlerDatabase.prototype.createUser = function(username, email, password) {
|
||||
const result = this.engine.runStatement(`
|
||||
INSERT INTO users (username, email, password)
|
||||
VALUES ($username, $email, $password)
|
||||
`, {
|
||||
$username: username,
|
||||
$email: email,
|
||||
$password: password
|
||||
});
|
||||
return result.lastInsertRowid;
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getUser = function(userId) {
|
||||
return this.engine.runStatementGet(`
|
||||
SELECT * FROM users WHERE user_id = $userId
|
||||
`, {
|
||||
$userId: userId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getUserByUsername = function(username) {
|
||||
return this.engine.runStatementGet(`
|
||||
SELECT * FROM users WHERE username = $username
|
||||
`, {
|
||||
$username: username
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.updateUser = function (userId, username, email, roleId) {
|
||||
const existingUser = this.engine.runStatement(`
|
||||
SELECT user_id FROM users
|
||||
WHERE email = $email AND user_id != $userId
|
||||
`, {
|
||||
$email: email,
|
||||
$userId: userId
|
||||
});
|
||||
|
||||
if (existingUser.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Email address already in use by another user."
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
this.engine.transaction(() => {
|
||||
// Update user information
|
||||
this.engine.runStatement(`
|
||||
UPDATE users
|
||||
SET username = $username, email = $email
|
||||
WHERE user_id = $userId
|
||||
`, {
|
||||
$userId: userId,
|
||||
$username: username,
|
||||
$email: email
|
||||
});
|
||||
|
||||
if (roleId) {
|
||||
// Remove all existing roles for the user
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM user_roles
|
||||
WHERE user_id = $userId
|
||||
`, {
|
||||
$userId: userId
|
||||
});
|
||||
|
||||
// Add the new role
|
||||
this.engine.runStatement(`
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
VALUES ($userId, $roleId)
|
||||
`, {
|
||||
$userId: userId,
|
||||
$roleId: roleId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User profile and role updated successfully."
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to update user profile: " + error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.updateUserPassword = function (userId, newHash) {
|
||||
try {
|
||||
this.engine.runStatement(`
|
||||
UPDATE users
|
||||
SET password = $newHash
|
||||
WHERE user_id = $userId
|
||||
`, {
|
||||
$userId: userId,
|
||||
$newHash: newHash,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Password updated successfully."
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to update password: " + error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.deleteUser = function(userId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM users WHERE user_id = $userId
|
||||
`, {
|
||||
$userId: userId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.listUsers = function() {
|
||||
return this.engine.runStatementGetAll(`
|
||||
SELECT * FROM users ORDER BY username
|
||||
`);
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.createOrUpdateUserSession = function(userId, sessionId) {
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
|
||||
// First, try to update an existing session
|
||||
const updateResult = this.engine.runStatement(`
|
||||
UPDATE sessions
|
||||
SET session_id = $sessionId, last_accessed = $timestamp
|
||||
WHERE user_id = $userId
|
||||
`, {
|
||||
$userId: userId,
|
||||
$sessionId: sessionId,
|
||||
$timestamp: currentTimestamp
|
||||
});
|
||||
|
||||
// If no existing session was updated, create a new one
|
||||
if (updateResult.changes === 0) {
|
||||
this.engine.runStatement(`
|
||||
INSERT INTO sessions (user_id, session_id, created_at, last_accessed)
|
||||
VALUES ($userId, $sessionId, $timestamp, $timestamp)
|
||||
`, {
|
||||
$userId: userId,
|
||||
$sessionId: sessionId,
|
||||
$timestamp: currentTimestamp
|
||||
});
|
||||
}
|
||||
|
||||
return sessionId;
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) {
|
||||
// First, get the user_id from the sessions table
|
||||
const sessionResult = this.engine.runStatementGet(`
|
||||
SELECT user_id, last_accessed
|
||||
FROM sessions
|
||||
WHERE session_id = $sessionId
|
||||
`, {
|
||||
$sessionId: sessionId
|
||||
});
|
||||
|
||||
if (!sessionResult) {
|
||||
return null; // Session not found
|
||||
}
|
||||
|
||||
const lastAccessed = new Date(sessionResult.last_accessed);
|
||||
const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
if (new Date() - lastAccessed > expirationTime) {
|
||||
// Session has expired
|
||||
this.deleteSession(sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update the last_accessed timestamp
|
||||
const currentTimestamp = new Date().toISOString();
|
||||
this.engine.runStatement(`
|
||||
UPDATE sessions
|
||||
SET last_accessed = $timestamp
|
||||
WHERE session_id = $sessionId
|
||||
`, {
|
||||
$sessionId: sessionId,
|
||||
$timestamp: currentTimestamp
|
||||
});
|
||||
|
||||
const userResult = this.engine.runStatementGet(`
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE user_id = $userId
|
||||
`, {
|
||||
$userId: sessionResult.user_id
|
||||
});
|
||||
|
||||
if (!userResult) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return userResult;
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.deleteSession = function(sessionId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM sessions
|
||||
WHERE session_id = $sessionId
|
||||
`, {
|
||||
$sessionId: sessionId
|
||||
});
|
||||
};
|
||||
|
||||
// Group CRUD operations
|
||||
SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) {
|
||||
const result = this.engine.runStatement(`
|
||||
INSERT INTO groups (group_name, description)
|
||||
VALUES ($groupName, $description)
|
||||
`, {
|
||||
$groupName: groupName,
|
||||
$description: description
|
||||
});
|
||||
return result.lastInsertRowid;
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getGroup = function(groupId) {
|
||||
return this.engine.runStatementGet(`
|
||||
SELECT * FROM groups WHERE group_id = $groupId
|
||||
`, {
|
||||
$groupId: groupId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.updateGroup = function(groupId, groupName, description) {
|
||||
this.engine.runStatement(`
|
||||
UPDATE groups
|
||||
SET group_name = $groupName, description = $description
|
||||
WHERE group_id = $groupId
|
||||
`, {
|
||||
$groupId: groupId,
|
||||
$groupName: groupName,
|
||||
$description: description
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.deleteGroup = function(groupId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM groups WHERE group_id = $groupId
|
||||
`, {
|
||||
$groupId: groupId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.listGroups = function() {
|
||||
return this.engine.runStatementGetAll(`
|
||||
SELECT * FROM groups ORDER BY group_name
|
||||
`);
|
||||
};
|
||||
|
||||
// Role CRUD operations
|
||||
SqlTiddlerDatabase.prototype.createRole = function(roleName, description) {
|
||||
const result = this.engine.runStatement(`
|
||||
INSERT OR IGNORE INTO roles (role_name, description)
|
||||
VALUES ($roleName, $description)
|
||||
`, {
|
||||
$roleName: roleName,
|
||||
$description: description
|
||||
});
|
||||
return result.lastInsertRowid;
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getRole = function(roleId) {
|
||||
return this.engine.runStatementGet(`
|
||||
SELECT * FROM roles WHERE role_id = $roleId
|
||||
`, {
|
||||
$roleId: roleId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getRoleByName = function(roleName) {
|
||||
return this.engine.runStatementGet(`
|
||||
SELECT * FROM roles WHERE role_name = $roleName
|
||||
`, {
|
||||
$roleName: roleName
|
||||
});
|
||||
}
|
||||
|
||||
SqlTiddlerDatabase.prototype.updateRole = function(roleId, roleName, description) {
|
||||
this.engine.runStatement(`
|
||||
UPDATE roles
|
||||
SET role_name = $roleName, description = $description
|
||||
WHERE role_id = $roleId
|
||||
`, {
|
||||
$roleId: roleId,
|
||||
$roleName: roleName,
|
||||
$description: description
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.deleteRole = function(roleId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM roles WHERE role_id = $roleId
|
||||
`, {
|
||||
$roleId: roleId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.listRoles = function() {
|
||||
return this.engine.runStatementGetAll(`
|
||||
SELECT * FROM roles ORDER BY role_name
|
||||
`);
|
||||
};
|
||||
|
||||
// Permission CRUD operations
|
||||
SqlTiddlerDatabase.prototype.createPermission = function(permissionName, description) {
|
||||
const result = this.engine.runStatement(`
|
||||
INSERT OR IGNORE INTO permissions (permission_name, description)
|
||||
VALUES ($permissionName, $description)
|
||||
`, {
|
||||
$permissionName: permissionName,
|
||||
$description: description
|
||||
});
|
||||
return result.lastInsertRowid;
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getPermission = function(permissionId) {
|
||||
return this.engine.runStatementGet(`
|
||||
SELECT * FROM permissions WHERE permission_id = $permissionId
|
||||
`, {
|
||||
$permissionId: permissionId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getPermissionByName = function(permissionName) {
|
||||
return this.engine.runStatementGet(`
|
||||
SELECT * FROM permissions WHERE permission_name = $permissionName
|
||||
`, {
|
||||
$permissionName: permissionName
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.updatePermission = function(permissionId, permissionName, description) {
|
||||
this.engine.runStatement(`
|
||||
UPDATE permissions
|
||||
SET permission_name = $permissionName, description = $description
|
||||
WHERE permission_id = $permissionId
|
||||
`, {
|
||||
$permissionId: permissionId,
|
||||
$permissionName: permissionName,
|
||||
$description: description
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.deletePermission = function(permissionId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM permissions WHERE permission_id = $permissionId
|
||||
`, {
|
||||
$permissionId: permissionId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.listPermissions = function() {
|
||||
return this.engine.runStatementGetAll(`
|
||||
SELECT * FROM permissions ORDER BY permission_name
|
||||
`);
|
||||
};
|
||||
|
||||
// ACL CRUD operations
|
||||
SqlTiddlerDatabase.prototype.createACL = function(entityName, entityType, roleId, permissionId) {
|
||||
if(!entityName.startsWith("$:/")) {
|
||||
const result = this.engine.runStatement(`
|
||||
INSERT OR IGNORE INTO acl (entity_name, entity_type, role_id, permission_id)
|
||||
VALUES ($entityName, $entityType, $roleId, $permissionId)
|
||||
`,
|
||||
{
|
||||
$entityName: entityName,
|
||||
$entityType: entityType,
|
||||
$roleId: roleId,
|
||||
$permissionId: permissionId
|
||||
});
|
||||
return result.lastInsertRowid;
|
||||
}
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getACL = function(aclId) {
|
||||
return this.engine.runStatementGet(`
|
||||
SELECT * FROM acl WHERE acl_id = $aclId
|
||||
`, {
|
||||
$aclId: aclId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.updateACL = function(aclId, entityId, entityType, roleId, permissionId) {
|
||||
this.engine.runStatement(`
|
||||
UPDATE acl
|
||||
SET entity_name = $entityId, entity_type = $entityType,
|
||||
role_id = $roleId, permission_id = $permissionId
|
||||
WHERE acl_id = $aclId
|
||||
`, {
|
||||
$aclId: aclId,
|
||||
$entityId: entityId,
|
||||
$entityType: entityType,
|
||||
$roleId: roleId,
|
||||
$permissionId: permissionId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.deleteACL = function(aclId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM acl WHERE acl_id = $aclId
|
||||
`, {
|
||||
$aclId: aclId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.listACLs = function() {
|
||||
return this.engine.runStatementGetAll(`
|
||||
SELECT * FROM acl ORDER BY entity_type, entity_name
|
||||
`);
|
||||
};
|
||||
|
||||
// Association management functions
|
||||
SqlTiddlerDatabase.prototype.addUserToGroup = function(userId, groupId) {
|
||||
this.engine.runStatement(`
|
||||
INSERT OR IGNORE INTO user_groups (user_id, group_id)
|
||||
VALUES ($userId, $groupId)
|
||||
`, {
|
||||
$userId: userId,
|
||||
$groupId: groupId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.isUserInGroup = function(userId, groupId) {
|
||||
const result = this.engine.runStatementGet(`
|
||||
SELECT 1 FROM user_groups
|
||||
WHERE user_id = $userId AND group_id = $groupId
|
||||
`, {
|
||||
$userId: userId,
|
||||
$groupId: groupId
|
||||
});
|
||||
return result !== undefined;
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.removeUserFromGroup = function(userId, groupId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM user_groups
|
||||
WHERE user_id = $userId AND group_id = $groupId
|
||||
`, {
|
||||
$userId: userId,
|
||||
$groupId: groupId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.addRoleToUser = function(userId, roleId) {
|
||||
this.engine.runStatement(`
|
||||
INSERT OR IGNORE INTO user_roles (user_id, role_id)
|
||||
VALUES ($userId, $roleId)
|
||||
`, {
|
||||
$userId: userId,
|
||||
$roleId: roleId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.removeRoleFromUser = function(userId, roleId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM user_roles
|
||||
WHERE user_id = $userId AND role_id = $roleId
|
||||
`, {
|
||||
$userId: userId,
|
||||
$roleId: roleId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.addRoleToGroup = function(groupId, roleId) {
|
||||
this.engine.runStatement(`
|
||||
INSERT OR IGNORE INTO group_roles (group_id, role_id)
|
||||
VALUES ($groupId, $roleId)
|
||||
`, {
|
||||
$groupId: groupId,
|
||||
$roleId: roleId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.removeRoleFromGroup = function(groupId, roleId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM group_roles
|
||||
WHERE group_id = $groupId AND role_id = $roleId
|
||||
`, {
|
||||
$groupId: groupId,
|
||||
$roleId: roleId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.addPermissionToRole = function(roleId, permissionId) {
|
||||
this.engine.runStatement(`
|
||||
INSERT OR IGNORE INTO role_permissions (role_id, permission_id)
|
||||
VALUES ($roleId, $permissionId)
|
||||
`, {
|
||||
$roleId: roleId,
|
||||
$permissionId: permissionId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.removePermissionFromRole = function(roleId, permissionId) {
|
||||
this.engine.runStatement(`
|
||||
DELETE FROM role_permissions
|
||||
WHERE role_id = $roleId AND permission_id = $permissionId
|
||||
`, {
|
||||
$roleId: roleId,
|
||||
$permissionId: permissionId
|
||||
});
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getUserRoles = function(userId) {
|
||||
const query = `
|
||||
SELECT r.role_id, r.role_name
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON ur.role_id = r.role_id
|
||||
WHERE ur.user_id = $userId
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
return this.engine.runStatementGet(query, { $userId: userId });
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.isRoleInUse = function(roleId) {
|
||||
// Check if the role is assigned to any users
|
||||
const userRoleCheck = this.engine.runStatementGet(`
|
||||
SELECT 1
|
||||
FROM user_roles
|
||||
WHERE role_id = $roleId
|
||||
LIMIT 1
|
||||
`, {
|
||||
$roleId: roleId
|
||||
});
|
||||
|
||||
if(userRoleCheck) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the role is used in any ACLs
|
||||
const aclRoleCheck = this.engine.runStatementGet(`
|
||||
SELECT 1
|
||||
FROM acl
|
||||
WHERE role_id = $roleId
|
||||
LIMIT 1
|
||||
`, {
|
||||
$roleId: roleId
|
||||
});
|
||||
|
||||
if(aclRoleCheck) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we've reached this point, the role is not in use
|
||||
return false;
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.getRoleById = function(roleId) {
|
||||
const role = this.engine.runStatementGet(`
|
||||
SELECT role_id, role_name, description
|
||||
FROM roles
|
||||
WHERE role_id = $roleId
|
||||
`, {
|
||||
$roleId: roleId
|
||||
});
|
||||
|
||||
return role;
|
||||
};
|
||||
|
||||
exports.SqlTiddlerDatabase = SqlTiddlerDatabase;
|
||||
|
||||
})();
|
||||
})();
|
||||
|
@ -104,6 +104,124 @@ function runSqlDatabaseTests(engine) {
|
||||
expect(sqlTiddlerDatabase.saveRecipeTiddler({title: "More", text: "None"},"recipe-rho")).toEqual({tiddler_id: 7, bag_name: 'bag-beta'});
|
||||
expect(sqlTiddlerDatabase.getRecipeTiddler("More","recipe-rho").tiddler).toEqual({title: "More", text: "None"});
|
||||
});
|
||||
|
||||
it("should manage users correctly", function() {
|
||||
console.log("should manage users correctly")
|
||||
// Create users
|
||||
const userId1 = sqlTiddlerDatabase.createUser("john_doe", "john@example.com", "pass123");
|
||||
const userId2 = sqlTiddlerDatabase.createUser("jane_doe", "jane@example.com", "pass123");
|
||||
|
||||
// Retrieve users
|
||||
const user1 = sqlTiddlerDatabase.getUser(userId1);
|
||||
expect(user1.user_id).toBe(userId1);
|
||||
expect(user1.username).toBe("john_doe");
|
||||
expect(user1.email).toBe("john@example.com");
|
||||
expect(user1.created_at).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); // Match timestamp format
|
||||
expect(user1.last_login).toBeNull();
|
||||
|
||||
// Update user
|
||||
sqlTiddlerDatabase.updateUser(userId1, "john_updated", "john_updated@example.com");
|
||||
expect(sqlTiddlerDatabase.getUser(userId1).username).toBe("john_updated");
|
||||
expect(sqlTiddlerDatabase.getUser(userId1).email).toBe("john_updated@example.com");
|
||||
|
||||
// List users
|
||||
const users = sqlTiddlerDatabase.listUsers();
|
||||
expect(users.length).toBe(2);
|
||||
expect(users[0].username).toBe("jane_doe");
|
||||
expect(users[1].username).toBe("john_updated");
|
||||
|
||||
// Delete user
|
||||
sqlTiddlerDatabase.deleteUser(userId2);
|
||||
// expect(sqlTiddlerDatabase.getUser(userId2)).toBe(null || undefined);
|
||||
});
|
||||
|
||||
it("should manage groups correctly", function() {
|
||||
console.log("should manage groups correctly")
|
||||
// Create groups
|
||||
const groupId1 = sqlTiddlerDatabase.createGroup("Editors", "Can edit content");
|
||||
const groupId2 = sqlTiddlerDatabase.createGroup("Viewers", "Can view content");
|
||||
|
||||
// Retrieve groups
|
||||
expect(sqlTiddlerDatabase.getGroup(groupId1)).toEqual({
|
||||
group_id: groupId1,
|
||||
group_name: "Editors",
|
||||
description: "Can edit content"
|
||||
});
|
||||
|
||||
// Update group
|
||||
sqlTiddlerDatabase.updateGroup(groupId1, "Super Editors", "Can edit all content");
|
||||
expect(sqlTiddlerDatabase.getGroup(groupId1).group_name).toBe("Super Editors");
|
||||
expect(sqlTiddlerDatabase.getGroup(groupId1).description).toBe("Can edit all content");
|
||||
|
||||
// List groups
|
||||
const groups = sqlTiddlerDatabase.listGroups();
|
||||
expect(groups.length).toBe(2);
|
||||
expect(groups[0].group_name).toBe("Super Editors");
|
||||
expect(groups[1].group_name).toBe("Viewers");
|
||||
|
||||
// Delete group
|
||||
sqlTiddlerDatabase.deleteGroup(groupId2);
|
||||
// expect(sqlTiddlerDatabase.getGroup(groupId2)).toBe(null || undefined);
|
||||
});
|
||||
|
||||
|
||||
it("should manage roles correctly", function() {
|
||||
console.log("should manage roles correctly")
|
||||
// Create roles
|
||||
const roleId1 = sqlTiddlerDatabase.createRole("Admin" + Date.now(), "Full access");
|
||||
const roleId2 = sqlTiddlerDatabase.createRole("Editor" + Date.now(), "Can edit content");
|
||||
|
||||
// Retrieve roles
|
||||
expect(sqlTiddlerDatabase.getRole(roleId1)).toEqual({
|
||||
role_id: roleId1,
|
||||
role_name: jasmine.stringMatching(/^Admin\d+$/),
|
||||
description: "Full access"
|
||||
});
|
||||
|
||||
// Update role
|
||||
sqlTiddlerDatabase.updateRole(roleId1, "Super Admin" + Date.now(), "God-like powers");
|
||||
expect(sqlTiddlerDatabase.getRole(roleId1).role_name).toMatch(/^Super Admin\d+$/);
|
||||
expect(sqlTiddlerDatabase.getRole(roleId1).description).toBe("God-like powers");
|
||||
|
||||
// List roles
|
||||
const roles = sqlTiddlerDatabase.listRoles();
|
||||
expect(roles.length).toBeGreaterThan(0);
|
||||
// expect(roles[0].role_name).toMatch(/^Editor\d+$/);
|
||||
// expect(roles[1].role_name).toMatch(/^Super Admin\d+$/);
|
||||
|
||||
// Delete role
|
||||
sqlTiddlerDatabase.deleteRole(roleId2);
|
||||
// expect(sqlTiddlerDatabase.getRole(roleId2)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should manage permissions correctly", function() {
|
||||
console.log("should manage permissions correctly")
|
||||
// Create permissions
|
||||
const permissionId1 = sqlTiddlerDatabase.createPermission("read_tiddlers" + Date.now(), "Can read tiddlers");
|
||||
const permissionId2 = sqlTiddlerDatabase.createPermission("write_tiddlers" + Date.now(), "Can write tiddlers");
|
||||
|
||||
// Retrieve permissions
|
||||
expect(sqlTiddlerDatabase.getPermission(permissionId1)).toEqual({
|
||||
permission_id: permissionId1,
|
||||
permission_name: jasmine.stringMatching(/^read_tiddlers\d+$/),
|
||||
description: "Can read tiddlers"
|
||||
});
|
||||
|
||||
// Update permission
|
||||
sqlTiddlerDatabase.updatePermission(permissionId1, "read_all_tiddlers" + Date.now(), "Can read all tiddlers");
|
||||
expect(sqlTiddlerDatabase.getPermission(permissionId1).permission_name).toMatch(/^read_all_tiddlers\d+$/);
|
||||
expect(sqlTiddlerDatabase.getPermission(permissionId1).description).toBe("Can read all tiddlers");
|
||||
|
||||
// List permissions
|
||||
const permissions = sqlTiddlerDatabase.listPermissions();
|
||||
expect(permissions.length).toBeGreaterThan(0);
|
||||
expect(permissions[0].permission_name).toMatch(/^read_all_tiddlers\d+$/);
|
||||
expect(permissions[1].permission_name).toMatch(/^write_tiddlers\d+$/);
|
||||
|
||||
// Delete permission
|
||||
sqlTiddlerDatabase.deletePermission(permissionId2);
|
||||
// expect(sqlTiddlerDatabase.getPermission(permissionId2)).toBeUndefined();
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -14,151 +14,151 @@ if(typeof window === 'undefined' && typeof process !== 'undefined' && process.ve
|
||||
var AttachmentStore = require('$:/plugins/tiddlywiki/multiwikiserver/store/attachments.js').AttachmentStore;
|
||||
const {Buffer} = require('buffer');
|
||||
|
||||
function generateFileWithSize(filePath, sizeInBytes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var buffer = Buffer.alloc(sizeInBytes);
|
||||
for(var i = 0; i < sizeInBytes; i++) {
|
||||
buffer[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
function generateFileWithSize(filePath, sizeInBytes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var buffer = Buffer.alloc(sizeInBytes);
|
||||
for(var i = 0; i < sizeInBytes; i++) {
|
||||
buffer[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
|
||||
fs.writeFile(filePath, buffer, (err) => {
|
||||
if(err) {
|
||||
console.error('Error writing file:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('File '+filePath+' generated with size '+sizeInBytes+' bytes');
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if(err) {
|
||||
console.error('Error reading file:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
fs.writeFile(filePath, buffer, (err) => {
|
||||
if(err) {
|
||||
console.error('Error writing file:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('File '+filePath+' generated with size '+sizeInBytes+' bytes');
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if(err) {
|
||||
console.error('Error reading file:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
if($tw.node) {
|
||||
describe('AttachmentStore', function() {
|
||||
var storePath = './editions/test/test-store';
|
||||
var attachmentStore = new AttachmentStore({ storePath: storePath });
|
||||
var originalTimeout;
|
||||
(function() {
|
||||
'use strict';
|
||||
if($tw.node) {
|
||||
describe('AttachmentStore', function() {
|
||||
var storePath = './editions/test/test-store';
|
||||
var attachmentStore = new AttachmentStore({ storePath: storePath });
|
||||
var originalTimeout;
|
||||
|
||||
beforeAll(function() {
|
||||
const dirPath = path.dirname(`${storePath}/files`);
|
||||
if(!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000;
|
||||
});
|
||||
beforeAll(function() {
|
||||
const dirPath = path.dirname(`${storePath}/files`);
|
||||
if(!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000;
|
||||
});
|
||||
|
||||
afterAll(function() {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
|
||||
fs.readdirSync(storePath).forEach(function(file) {
|
||||
var filePath = path.join(storePath, file);
|
||||
if(fs.lstatSync(filePath).isFile()) {
|
||||
fs.unlinkSync(filePath);
|
||||
} else if(fs.lstatSync(filePath).isDirectory()) {
|
||||
fs.rmdirSync(filePath, { recursive: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
afterAll(function() {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
|
||||
fs.readdirSync(storePath).forEach(function(file) {
|
||||
var filePath = path.join(storePath, file);
|
||||
if(fs.lstatSync(filePath).isFile()) {
|
||||
fs.unlinkSync(filePath);
|
||||
} else if(fs.lstatSync(filePath).isDirectory()) {
|
||||
fs.rmdirSync(filePath, { recursive: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('isValidAttachmentName', function() {
|
||||
expect(attachmentStore.isValidAttachmentName('abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890')).toBe(true);
|
||||
expect(attachmentStore.isValidAttachmentName('invalid-name')).toBe(false);
|
||||
});
|
||||
it('isValidAttachmentName', function() {
|
||||
expect(attachmentStore.isValidAttachmentName('abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890')).toBe(true);
|
||||
expect(attachmentStore.isValidAttachmentName('invalid-name')).toBe(false);
|
||||
});
|
||||
|
||||
it('saveAttachment', function() {
|
||||
var options = {
|
||||
text: 'Hello, World!',
|
||||
type: 'text/plain',
|
||||
reference: 'test-reference',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
assert.strictEqual(contentHash.length, 64);
|
||||
assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true);
|
||||
});
|
||||
|
||||
it('adoptAttachment', function() {
|
||||
var incomingFilepath = path.resolve(storePath, 'incoming-file.txt');
|
||||
fs.writeFileSync(incomingFilepath, 'Hello, World!');
|
||||
var type = 'text/plain';
|
||||
var hash = 'abcdef0123456789abcdef0123456789';
|
||||
var _canonical_uri = 'test-canonical-uri';
|
||||
attachmentStore.adoptAttachment(incomingFilepath, type, hash, _canonical_uri);
|
||||
expect(fs.existsSync(path.resolve(storePath, 'files', hash))).toBe(true);
|
||||
});
|
||||
|
||||
it('getAttachmentStream', function() {
|
||||
var options = {
|
||||
text: 'Hello, World!',
|
||||
type: 'text/plain',
|
||||
filename: 'data.txt',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
var stream = attachmentStore.getAttachmentStream(contentHash);
|
||||
expect(stream).not.toBeNull();
|
||||
expect(stream.type).toBe('text/plain');
|
||||
});
|
||||
it('saveAttachment', function() {
|
||||
var options = {
|
||||
text: 'Hello, World!',
|
||||
type: 'text/plain',
|
||||
reference: 'test-reference',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
assert.strictEqual(contentHash.length, 64);
|
||||
assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true);
|
||||
});
|
||||
|
||||
it('adoptAttachment', function() {
|
||||
var incomingFilepath = path.resolve(storePath, 'incoming-file.txt');
|
||||
fs.writeFileSync(incomingFilepath, 'Hello, World!');
|
||||
var type = 'text/plain';
|
||||
var hash = 'abcdef0123456789abcdef0123456789';
|
||||
var _canonical_uri = 'test-canonical-uri';
|
||||
attachmentStore.adoptAttachment(incomingFilepath, type, hash, _canonical_uri);
|
||||
expect(fs.existsSync(path.resolve(storePath, 'files', hash))).toBe(true);
|
||||
});
|
||||
|
||||
it('getAttachmentStream', function() {
|
||||
var options = {
|
||||
text: 'Hello, World!',
|
||||
type: 'text/plain',
|
||||
filename: 'data.txt',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
var stream = attachmentStore.getAttachmentStream(contentHash);
|
||||
expect(stream).not.toBeNull();
|
||||
expect(stream.type).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('getAttachmentFileSize', function() {
|
||||
var options = {
|
||||
text: 'Hello, World!',
|
||||
type: 'text/plain',
|
||||
reference: 'test-reference',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
var fileSize = attachmentStore.getAttachmentFileSize(contentHash);
|
||||
expect(fileSize).toBe(13);
|
||||
});
|
||||
it('getAttachmentFileSize', function() {
|
||||
var options = {
|
||||
text: 'Hello, World!',
|
||||
type: 'text/plain',
|
||||
reference: 'test-reference',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
var fileSize = attachmentStore.getAttachmentFileSize(contentHash);
|
||||
expect(fileSize).toBe(13);
|
||||
});
|
||||
|
||||
it('getAttachmentMetadata', function() {
|
||||
var options = {
|
||||
text: 'Hello, World!',
|
||||
type: 'text/plain',
|
||||
filename: 'data.txt',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
var metadata = attachmentStore.getAttachmentMetadata(contentHash);
|
||||
expect(metadata).not.toBeNull();
|
||||
expect(metadata.type).toBe('text/plain');
|
||||
expect(metadata.filename).toBe('data.txt');
|
||||
});
|
||||
it('getAttachmentMetadata', function() {
|
||||
var options = {
|
||||
text: 'Hello, World!',
|
||||
type: 'text/plain',
|
||||
filename: 'data.txt',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
var metadata = attachmentStore.getAttachmentMetadata(contentHash);
|
||||
expect(metadata).not.toBeNull();
|
||||
expect(metadata.type).toBe('text/plain');
|
||||
expect(metadata.filename).toBe('data.txt');
|
||||
});
|
||||
|
||||
it('saveAttachment large file', async function() {
|
||||
var sizeInMB = 10
|
||||
const file = await generateFileWithSize('./editions/test/test-store/large-file.txt', 1024 * 1024 * sizeInMB)
|
||||
var options = {
|
||||
text: file,
|
||||
type: 'application/octet-stream',
|
||||
reference: 'test-reference',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
assert.strictEqual(contentHash.length, 64);
|
||||
assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true);
|
||||
});
|
||||
it('saveAttachment large file', async function() {
|
||||
var sizeInMB = 10
|
||||
const file = await generateFileWithSize('./editions/test/test-store/large-file.txt', 1024 * 1024 * sizeInMB)
|
||||
var options = {
|
||||
text: file,
|
||||
type: 'application/octet-stream',
|
||||
reference: 'test-reference',
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
assert.strictEqual(contentHash.length, 64);
|
||||
assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true);
|
||||
});
|
||||
|
||||
it('saveAttachment multiple large files', async function() {
|
||||
var sizeInMB = 10;
|
||||
var numFiles = 5;
|
||||
for (var i = 0; i < numFiles; i++) {
|
||||
const file = await generateFileWithSize(`./editions/test/test-store/large-file-${i}.txt`, 1024 * 1024 * sizeInMB);
|
||||
var options = {
|
||||
text: file,
|
||||
type: 'application/octet-stream',
|
||||
reference: `test-reference-${i}`,
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
assert.strictEqual(contentHash.length, 64);
|
||||
assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true);
|
||||
}
|
||||
});
|
||||
it('saveAttachment multiple large files', async function() {
|
||||
var sizeInMB = 10;
|
||||
var numFiles = 5;
|
||||
for (var i = 0; i < numFiles; i++) {
|
||||
const file = await generateFileWithSize(`./editions/test/test-store/large-file-${i}.txt`, 1024 * 1024 * sizeInMB);
|
||||
var options = {
|
||||
text: file,
|
||||
type: 'application/octet-stream',
|
||||
reference: `test-reference-${i}`,
|
||||
};
|
||||
var contentHash = attachmentStore.saveAttachment(options);
|
||||
assert.strictEqual(contentHash.length, 64);
|
||||
assert.strictEqual(fs.existsSync(path.resolve(storePath, 'files', contentHash)), true);
|
||||
}
|
||||
});
|
||||
|
||||
it('getAttachmentStream multiple large files', async function() {
|
||||
var sizeInMB = 10;
|
||||
|
19
plugins/tiddlywiki/multiwikiserver/scripts/admin-dropdown.js
Normal file
19
plugins/tiddlywiki/multiwikiserver/scripts/admin-dropdown.js
Normal file
@ -0,0 +1,19 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/scripts/admin-dropdown.js
|
||||
type: application/javascript
|
||||
|
||||
\*/
|
||||
|
||||
(function () {
|
||||
document.addEventListener("click", function (event) {
|
||||
var dropdown = document.querySelector(".mws-admin-dropdown-content");
|
||||
var dropbtn = document.querySelector(".mws-admin-dropbtn");
|
||||
if(!event.target.matches(".mws-admin-dropbtn")) {
|
||||
if(dropdown.style.display === "block") {
|
||||
dropdown.style.display = "none";
|
||||
}
|
||||
} else {
|
||||
dropdown.style.display = dropdown.style.display === "block" ? "none" : "block";
|
||||
}
|
||||
});
|
||||
})();
|
@ -0,0 +1,93 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/templates/add-user-form
|
||||
|
||||
<h1>Add New User</h1>
|
||||
<form method="POST" action="/admin/post-user">
|
||||
<div class="form-group">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">Confirm Password:</label>
|
||||
<input type="password" id="confirmPassword" name="confirmPassword" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<$button class="btn btn-primary">
|
||||
Add User
|
||||
<<add-user-actions>>
|
||||
<$action-sendmessage $message="tm-close-tiddler"/>
|
||||
</$button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.add-user-form {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.add-user-form h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
@ -21,7 +21,11 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
|
||||
</$genesis>
|
||||
\end
|
||||
|
||||
! Wikis Available Here
|
||||
<$tiddler tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/mws-header">
|
||||
<$set name="page-title" value="Wikis Available Here">
|
||||
<$transclude/>
|
||||
</$set>
|
||||
</$tiddler>
|
||||
|
||||
<ul class="mws-vertical-list">
|
||||
<$list filter="[<recipe-list>jsonindexes[]] :sort[<currentTiddler>jsonget[recipe_name]]" variable="recipe-index">
|
||||
@ -69,7 +73,6 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
|
||||
</li>
|
||||
</$list>
|
||||
</ul>
|
||||
|
||||
<form action="/recipes" method="post" class="mws-form">
|
||||
<div class="mws-form-heading">
|
||||
Create a new recipe or modify and existing one
|
||||
@ -149,4 +152,78 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
|
||||
<%endif%>
|
||||
<label for="chkShowSystem">Show system bags</label>
|
||||
<button type="submit">Update</button>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.mws-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #f0f0f0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mws-user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mws-logout-form {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.mws-logout-button {
|
||||
padding: 5px 10px;
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mws-logout-button:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
|
||||
.mws-admin-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.mws-admin-dropbtn {
|
||||
color: white;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mws-admin-dropbtn:hover, .mws-admin-dropbtn:focus {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mws-admin-dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f1f1f1;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mws-admin-dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mws-admin-dropdown-content a:hover {background-color: #ddd;}
|
||||
|
||||
.mws-admin-dropdown:hover .mws-admin-dropdown-content {display: block;}
|
||||
|
||||
.mws-admin-dropdown:hover {background-color: #2980B9;}
|
||||
</style>
|
||||
|
124
plugins/tiddlywiki/multiwikiserver/templates/get-users.tid
Normal file
124
plugins/tiddlywiki/multiwikiserver/templates/get-users.tid
Normal file
@ -0,0 +1,124 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-users
|
||||
|
||||
\define edit-user-actions(user-id)
|
||||
<$action-sendmessage $message="tm-modal" $param="$:/plugins/tiddlywiki/multiwikiserver/templates/edit-user-modal" user-id=<<user-id>>/>
|
||||
\end
|
||||
|
||||
\define delete-user-actions(user-id)
|
||||
<$action-sendmessage $message="tm-server-request"
|
||||
method="DELETE"
|
||||
url={{{ [[$:/admin/users/]addsuffix<user-id>] }}}
|
||||
redirectAfterSuccess="/admin/users"/>
|
||||
\end
|
||||
|
||||
<$tiddler tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/mws-header">
|
||||
<$set name="page-title" value="User Management">
|
||||
<$transclude/>
|
||||
</$set>
|
||||
</$tiddler>
|
||||
|
||||
<div class="users-container">
|
||||
<div class="users-list">
|
||||
<$list filter="[<user-list>jsonindexes[]]" variable="user-index">
|
||||
<$let currentUser={{{ [<user-list>jsonextract<user-index>] }}}>
|
||||
<$set name="user-id" value={{{ [<currentUser>jsonget[user_id]] }}}>
|
||||
<a href={{{ [[/admin/users/]addsuffix<user-id>] }}} class="user-item">
|
||||
<div class="user-info">
|
||||
<span class="user-name">
|
||||
<$text text={{{ [<currentUser>jsonget[username]] }}}/>
|
||||
</span>
|
||||
<span class="user-email">
|
||||
<$text text={{{ [<currentUser>jsonget[email]] }}}/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<span class="user-created">
|
||||
Created: <$text text={{{ [<currentUser>jsonget[created_at]] }}}/>
|
||||
</span>
|
||||
<span class="user-last-login">
|
||||
Last Login: <$text text={{{ [<currentUser>jsonget[last_login]] }}}/>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</$set>
|
||||
</$let>
|
||||
</$list>
|
||||
</div>
|
||||
|
||||
<$list filter="[<user-is-admin>match[yes]]">
|
||||
<div class="add-user-card">
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/add-user-form" mode="inline"/>
|
||||
</div>
|
||||
</$list>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.users-container {
|
||||
margin: auto;
|
||||
max-width: 1200px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
}
|
||||
.users-list {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
}
|
||||
.add-user-card {
|
||||
width: 300px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
}
|
||||
.user-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 1rem 0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
.user-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
text-decoration: none;
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.user-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
.user-email {
|
||||
color: #666;
|
||||
}
|
||||
.user-details {
|
||||
font-size: 0.9em;
|
||||
color: #888;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.add-user-form {
|
||||
display: none; /* Hide the original add user button */
|
||||
}
|
||||
.tc-btn-big-green {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
}
|
||||
.tc-btn-big-green:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
</style>
|
267
plugins/tiddlywiki/multiwikiserver/templates/manage-acl.tid
Normal file
267
plugins/tiddlywiki/multiwikiserver/templates/manage-acl.tid
Normal file
@ -0,0 +1,267 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl
|
||||
|
||||
<$tiddler tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/mws-header">
|
||||
<$set name="page-title" value="Manage ACL">
|
||||
<$transclude />
|
||||
</$set>
|
||||
</$tiddler>
|
||||
|
||||
|
||||
<div class="container">
|
||||
<h2>Recipe ACL: <$text text={{{ [<recipe>jsonget[recipe_name]] }}}/></h2>
|
||||
<div class="acl-section">
|
||||
<div class="acl-form">
|
||||
<h3>Add Recipe ACL Record</h3>
|
||||
<form method="POST" action="/admin/post-acl">
|
||||
<input type="hidden" name="entity_type" value="recipe" />
|
||||
<input type="hidden" name="recipe_name" value={{{ [<recipe>jsonget[recipe_name]] }}}/>
|
||||
<input type="hidden" name="bag_name" value={{{ [<bag>jsonget[bag_name]] }}}/>
|
||||
<select name="role_id" class="tc-select">
|
||||
<option value="">Select Role</option>
|
||||
<$list filter="[<roles-list>jsonindexes[]]" variable="role-index">
|
||||
<$let role={{{ [<roles-list>jsonextract<role-index>] }}}>
|
||||
<option value={{{ [<role>jsonget[role_id]] }}}><$text text={{{ [<role>jsonget[role_name]] }}}/></option>
|
||||
</$let>
|
||||
</$list>
|
||||
</select>
|
||||
|
||||
<select name="permission_id" class="tc-select">
|
||||
<option value="">Select Permission</option>
|
||||
<$list filter="[<permissions-list>jsonindexes[]]" variable="permission-index">
|
||||
<$let permission={{{ [<permissions-list>jsonextract<permission-index>] }}}>
|
||||
<option value={{{ [<permission>jsonget[permission_id]] }}}><$text text={{{ [<permission>jsonget[permission_name]] }}}/></option>
|
||||
</$let>
|
||||
</$list>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="tc-btn-invisible btn-add">
|
||||
Add ACL Record
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="acl-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<th>Permission</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<$list filter="[<recipe-acl-records>jsonindexes[]]" variable="acl-index">
|
||||
<$let acl={{{ [<recipe-acl-records>jsonextract<acl-index>] }}}>
|
||||
<tr>
|
||||
<td>
|
||||
<$text text={{{ [<acl>jsonget[role_name]] }}}/>
|
||||
</td>
|
||||
<td>
|
||||
<$text text={{{ [<acl>jsonget[permission_name]] }}}/>
|
||||
(<small><$text text={{{ [<acl>jsonget[permission_description]] }}}/></small>)
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="/admin/delete-acl">
|
||||
<input type="hidden" name="acl_id" value={{{ [<acl>jsonget[acl_id]] }}}/>
|
||||
<input type="hidden" name="entity_type" value="recipe" />
|
||||
<input type="hidden" name="recipe_name" value={{{ [<recipe>jsonget[recipe_name]] }}}/>
|
||||
<input type="hidden" name="bag_name" value={{{ [<bag>jsonget[bag_name]] }}}/>
|
||||
<button type="submit" class="btn btn-delete">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</$let>
|
||||
</$list>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Bag ACL: <$text text={{{ [<bag>jsonget[bag_name]] }}}/></h2>
|
||||
<div class="acl-section">
|
||||
<div class="acl-form">
|
||||
<h3>Add Bag ACL Record</h3>
|
||||
<form method="POST" action="/admin/post-acl">
|
||||
<input type="hidden" name="entity_type" value="bag" />
|
||||
<input type="hidden" name="recipe_name" value={{{ [<recipe>jsonget[recipe_name]] }}}/>
|
||||
<input type="hidden" name="bag_name" value={{{ [<bag>jsonget[bag_name]] }}}/>
|
||||
<select name="role_id" class="tc-select">
|
||||
<option value="">Select Role</option>
|
||||
<$list filter="[<roles-list>jsonindexes[]]" variable="role-index">
|
||||
<$let role={{{ [<roles-list>jsonextract<role-index>] }}}>
|
||||
<option value={{{ [<role>jsonget[role_id]] }}}><$text text={{{ [<role>jsonget[role_name]] }}}/></option>
|
||||
</$let>
|
||||
</$list>
|
||||
</select>
|
||||
|
||||
<select name="permission_id" class="tc-select">
|
||||
<option value="">Select Permission</option>
|
||||
<$list filter="[<permissions-list>jsonindexes[]]" variable="permission-index">
|
||||
<$let permission={{{ [<permissions-list>jsonextract<permission-index>] }}}>
|
||||
<option value={{{ [<permission>jsonget[permission_id]] }}}><$text text={{{ [<permission>jsonget[permission_name]] }}}/></option>
|
||||
</$let>
|
||||
</$list>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="tc-btn-invisible btn-add">
|
||||
Add ACL Record
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="acl-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<th>Permission</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<$list filter="[<bag-acl-records>jsonindexes[]]" variable="acl-index">
|
||||
<$let acl={{{ [<bag-acl-records>jsonextract<acl-index>] }}}>
|
||||
<tr>
|
||||
<td>
|
||||
<$text text={{{ [<acl>jsonget[role_name]] }}}/>
|
||||
</td>
|
||||
<td>
|
||||
<$text text={{{ [<acl>jsonget[permission_name]] }}}/>
|
||||
(<small><$text text={{{ [<acl>jsonget[permission_description]] }}}/></small>)
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="/admin/delete-acl">
|
||||
<input type="hidden" name="acl_id" value={{{ [<acl>jsonget[acl_id]] }}}/>
|
||||
<input type="hidden" name="entity_type" value="bag" />
|
||||
<input type="hidden" name="recipe_name" value={{{ [<recipe>jsonget[recipe_name]] }}}/>
|
||||
<input type="hidden" name="bag_name" value={{{ [<bag>jsonget[bag_name]] }}}/>
|
||||
<button type="submit" class="btn btn-delete">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</$let>
|
||||
</$list>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.acl-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.acl-form {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.acl-table {
|
||||
flex: 2;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background-color: #e74c3c;
|
||||
color: white;
|
||||
padding: 4px 10px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background-color: #c0392b;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
select,
|
||||
.tc-btn-invisible {
|
||||
padding: 8px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.acl-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.acl-form,
|
||||
.acl-table {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
175
plugins/tiddlywiki/multiwikiserver/templates/manage-roles.tid
Normal file
175
plugins/tiddlywiki/multiwikiserver/templates/manage-roles.tid
Normal file
@ -0,0 +1,175 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-roles
|
||||
|
||||
\define add-role-actions()
|
||||
<$action-sendmessage $message="tm-server-request"
|
||||
method="POST"
|
||||
url="/admin/roles"
|
||||
headers="Content-Type: application/json"
|
||||
body={{{ [{"name": "$(newRoleName)$", "description": "$(newRoleDescription)$"}jsonify[]] }}}
|
||||
redirectAfterSuccess="/admin/roles"/>
|
||||
<$action-setfield $tiddler="$:/temp/newRoleName" text=""/>
|
||||
<$action-setfield $tiddler="$:/temp/newRoleDescription" text=""/>
|
||||
\end
|
||||
|
||||
\define edit-role-actions(role-id)
|
||||
<$action-sendmessage $message="tm-server-request"
|
||||
method="PUT"
|
||||
url={{{ [[$:/admin/roles/]addsuffix<role-id>] }}}
|
||||
headers="Content-Type: application/json"
|
||||
body={{{ [{"name": "$(newRoleName)$", "description": "$(newRoleDescription)$"}jsonify[]] }}}
|
||||
redirectAfterSuccess="/admin/roles"/>
|
||||
\end
|
||||
|
||||
\define delete-role-actions(role-id)
|
||||
<$action-sendmessage $message="tm-server-request"
|
||||
method="DELETE"
|
||||
url={{{ [[$:/admin/roles/]addsuffix<role-id>] }}}
|
||||
redirectAfterSuccess="/admin/roles"/>
|
||||
\end
|
||||
|
||||
<$tiddler tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/mws-header">
|
||||
<$set name="page-title" value="Manage Roles">
|
||||
<$transclude/>
|
||||
</$set>
|
||||
</$tiddler>
|
||||
|
||||
<div class="roles-container">
|
||||
<div class="roles-list">
|
||||
<h2>Existing Roles</h2>
|
||||
<$list filter="[<roles-list>jsonindexes[]]" variable="role-index">
|
||||
<$let role={{{ [<roles-list>jsonextract<role-index>] }}}>
|
||||
<div class="role-item">
|
||||
<div class="role-info">
|
||||
<span class="role-name">
|
||||
<$text text={{{ [<role>jsonget[role_name]] }}}/>
|
||||
</span>
|
||||
<span class="role-description">
|
||||
<$text text={{{ [<role>jsonget[description]] }}}/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="role-actions">
|
||||
<$button class="tc-btn-invisible btn-edit">
|
||||
Edit
|
||||
<$action-sendmessage $message="tm-modal" $param="$:/plugins/tiddlywiki/multiwikiserver/templates/edit-role-modal" role-id={{{ [<role>jsonget[role_id]] }}}/>
|
||||
</$button>
|
||||
<form method="POST" action="/admin/delete-role">
|
||||
<input type="hidden" name="role_id" value={{{ [<role>jsonget[role_id]] }}}/>
|
||||
<button type="submit" class="tc-btn-invisible btn-delete">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</$let>
|
||||
</$list>
|
||||
</div>
|
||||
|
||||
<div class="add-role-card">
|
||||
<h2>Add New Role</h2>
|
||||
<form method="POST" action="/admin/post-role" class="add-role-form">
|
||||
<input name="role_name" type="text" placeholder="Role Name" required/>
|
||||
<input name="role_description" type="text" placeholder="Role Description" required/>
|
||||
<button type="submit" class="tc-btn-invisible btn-add">Add Role</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.roles-container {
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.roles-list {
|
||||
flex: 1 1 60%;
|
||||
min-width: 300px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
}
|
||||
.add-role-card {
|
||||
flex: 1 1 30%;
|
||||
min-width: 250px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.role-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.role-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.role-name {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.role-description {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.role-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.role-actions button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.btn-edit {
|
||||
color: #007bff;
|
||||
}
|
||||
.btn-delete {
|
||||
color: #dc3545;
|
||||
}
|
||||
.btn-edit:hover, .btn-delete:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.add-role-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.add-role-form input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.btn-add {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.btn-add:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.roles-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.roles-list, .add-role-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,111 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account
|
||||
|
||||
<div class="user-profile-management">
|
||||
<h2>Manage Account</h2>
|
||||
<$set name="current-role-id" value={{{ [<user-role>jsonget[role_id]] }}}>
|
||||
<form class="user-profile-form" action="/update-user-profile" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" value={{{ [<user>jsonget[username]] }}} required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" value={{{ [<user>jsonget[email]] }}} required />
|
||||
</div>
|
||||
<% if [<user-is-admin>match[yes]] %>
|
||||
<div class="form-group">
|
||||
<label for="role">Role:</label>
|
||||
<select id="role" name="role" required>
|
||||
<$list filter="[<all-roles>jsonindexes[]]" variable="role-index">
|
||||
<$set name="role-id" value={{{ [<all-roles>jsonextract<role-index>jsonget[role_id]] }}}>
|
||||
<option value=<<role-id>>
|
||||
<$list filter="[<current-role-id>match<role-id>]" variable="ignore">selected</$list>
|
||||
<$text text={{{ [<all-roles>jsonextract<role-index>jsonget[role_name]] }}}/>
|
||||
</option>
|
||||
</$set>
|
||||
</$list>
|
||||
</select>
|
||||
</div>
|
||||
<% endif %>
|
||||
<button type="submit" class="update-profile-btn">Update Profile</button>
|
||||
</form>
|
||||
</$set>
|
||||
<hr />
|
||||
<h2>Change Password</h2>
|
||||
<form class="user-profile-form" action="/change-user-password" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password:</label>
|
||||
<input type="password" id="new-password" name="newPassword" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm New Password:</label>
|
||||
<input type="password" id="confirm-password" name="confirmPassword" required />
|
||||
</div>
|
||||
<button type="submit" class="update-password-btn">Change Password</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.user-profile-management {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.user-profile-management h2 {
|
||||
font-size: 1.2rem;
|
||||
color: #3498db;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-profile-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.user-profile-form .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-profile-form label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.user-profile-form input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.update-profile-btn,
|
||||
.update-password-btn {
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.update-password-btn {
|
||||
background: #00796b;
|
||||
}
|
||||
|
||||
.update-profile-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.update-password-btn:hover {
|
||||
background: #00695c;
|
||||
}
|
||||
|
||||
.user-profile-form select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
158
plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid
Normal file
158
plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid
Normal file
@ -0,0 +1,158 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
|
||||
|
||||
<$tiddler tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/mws-header">
|
||||
<$set name="page-title" value="User Profile">
|
||||
<$transclude/>
|
||||
</$set>
|
||||
</$tiddler>
|
||||
<div class="main-wrapper">
|
||||
<div class="user-profile-container">
|
||||
<div class="user-profile-header">
|
||||
<div class="user-profile-avatar">
|
||||
<$text text={{{ [<user>jsonget[username]substr[0,1]uppercase[]] }}}/>
|
||||
</div>
|
||||
<h1 class="user-profile-name"><$text text={{{ [<user>jsonget[username]] }}}/></h1>
|
||||
<p class="user-profile-email"><$text text={{{ [<user>jsonget[email]] }}}/></p>
|
||||
</div>
|
||||
|
||||
<div class="user-profile-details">
|
||||
<div class="user-profile-item">
|
||||
<span class="user-profile-label">User ID:</span>
|
||||
<span class="user-profile-value"><$text text={{{ [<user>jsonget[user_id]] }}}/></span>
|
||||
</div>
|
||||
<div class="user-profile-item">
|
||||
<span class="user-profile-label">Created At:</span>
|
||||
<span class="user-profile-value"><$text text={{{ [<user>jsonget[created_at]split[T]first[]] }}}/></span>
|
||||
</div>
|
||||
<div class="user-profile-item">
|
||||
<span class="user-profile-label">Last Login:</span>
|
||||
<span class="user-profile-value"><$text text={{{ [<user>jsonget[last_login]split[T]first[]] }}}/></span>
|
||||
</div>
|
||||
|
||||
<div class="user-profile-roles">
|
||||
<h2>User Role</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<$text text={{{ [<user-role>jsonget[role_name]] }}}/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if [<user-is-admin>match[yes]] %>
|
||||
<$tiddler tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account">
|
||||
<$transclude/>
|
||||
</$tiddler>
|
||||
<% elseif [<is-current-user-profile>match[yes]] %>
|
||||
<$tiddler tiddler="$:/plugins/tiddlywiki/multiwikiserver/templates/manage-user-account">
|
||||
<$transclude/>
|
||||
</$tiddler>
|
||||
<% endif %>
|
||||
|
||||
<$let flash-message={{{ [[$:/state/mws/flash-message]get[text]] }}}>
|
||||
<$reveal type="nomatch" state="$:/state/mws/flash-message" text="">
|
||||
<div class="flash-message">
|
||||
<$text text=<<flash-message>>/>
|
||||
</div>
|
||||
<$action-setfield $tiddler="$:/state/mws/flash-message" text=""/>
|
||||
</$reveal>
|
||||
</$let>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
max-width: 80vw;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.user-profile-container {
|
||||
flex: 1;
|
||||
margin: 2rem auto;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.user-profile-header {
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-profile-avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 1rem;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.user-profile-avatar * {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.user-profile-name {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-profile-email {
|
||||
font-size: 1rem;
|
||||
opacity: 0.8;
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.user-profile-details {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.user-profile-item {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-profile-label {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.user-profile-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-profile-roles {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.user-profile-roles h2 {
|
||||
font-size: 1.2rem;
|
||||
color: #3498db;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-profile-roles ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-profile-roles li {
|
||||
background: #f1f1f1;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
94
plugins/tiddlywiki/multiwikiserver/templates/mws-header.tid
Normal file
94
plugins/tiddlywiki/multiwikiserver/templates/mws-header.tid
Normal file
@ -0,0 +1,94 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/templates/mws-header
|
||||
|
||||
<div class="mws-header">
|
||||
<h1><$text text=<<page-title>>/></h1>
|
||||
<div class="mws-user-info">
|
||||
<span>Hello, <$text text=<<username>>/></span>
|
||||
<$list filter="[<user-is-admin>match[yes]]">
|
||||
<div class="mws-admin-dropdown">
|
||||
<button class="mws-admin-dropbtn">⚙️</button>
|
||||
<div class="mws-admin-dropdown-content">
|
||||
<a href="/admin/users">Manage Users</a>
|
||||
<a href="/admin/roles">Manage Roles</a>
|
||||
</div>
|
||||
</div>
|
||||
</$list>
|
||||
<form action="/logout" method="post" class="mws-logout-form">
|
||||
<input type="submit" value="Logout" class="mws-logout-button"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mws-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #f0f0f0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mws-user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mws-logout-form {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.mws-logout-button {
|
||||
padding: 5px 10px;
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mws-logout-button:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
|
||||
.mws-admin-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.mws-admin-dropbtn {
|
||||
color: white;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mws-admin-dropbtn:hover, .mws-admin-dropbtn:focus {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mws-admin-dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f1f1f1;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mws-admin-dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mws-admin-dropdown-content a:hover {background-color: #ddd;}
|
||||
|
||||
.mws-admin-dropdown:hover .mws-admin-dropdown-content {display: block;}
|
||||
|
||||
.mws-admin-dropdown:hover {background-color: #2980B9;}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user