mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-04-07 11:16:55 +00:00
remove list users command & added support for database in server options
This commit is contained in:
parent
f02c8562f0
commit
81f73de87d
@ -336,7 +336,7 @@ Get the browser location.hash. We don't use location.hash because of the way tha
|
||||
*/
|
||||
$tw.utils.getLocationHash = function() {
|
||||
var href = window.location.href;
|
||||
var idx = href.indexOf("#");
|
||||
var idx = href.indexOf('#');
|
||||
if(idx === -1) {
|
||||
return "#";
|
||||
} else if(href.substr(idx + 1,1) === "#" || href.substr(idx + 1,3) === "%23") {
|
||||
@ -605,7 +605,7 @@ var globalCheck =[
|
||||
" delete Object.prototype.__temp__;",
|
||||
" }",
|
||||
" delete Object.prototype.__temp__;",
|
||||
].join("\n");
|
||||
].join('\n');
|
||||
|
||||
/*
|
||||
Run code globally with specified context variables in scope
|
||||
@ -1997,7 +1997,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
|
||||
value = path.relative(rootPath, filename).split(path.sep).slice(0, -1);
|
||||
break;
|
||||
case "filepath":
|
||||
value = path.relative(rootPath, filename).split(path.sep).join("/");
|
||||
value = path.relative(rootPath, filename).split(path.sep).join('/');
|
||||
break;
|
||||
case "filename":
|
||||
value = path.basename(filename);
|
||||
@ -2623,7 +2623,7 @@ $tw.boot.executeNextStartupTask = function(callback) {
|
||||
}
|
||||
taskIndex++;
|
||||
}
|
||||
if(typeof callback === "function") {
|
||||
if(typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
return false;
|
||||
|
@ -11,9 +11,6 @@
|
||||
"tiddlywiki/snowwhite"
|
||||
],
|
||||
"build": {
|
||||
"--mws-list-users": [
|
||||
"--mws-list-users"
|
||||
],
|
||||
"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",
|
||||
|
@ -1,6 +1,5 @@
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/form
|
||||
|
||||
<$macrocall $name="loginForm"/>
|
||||
<form class="login-form" method="POST" action="/login">
|
||||
<input type="hidden" name="returnUrl" value=<<returnUrl>>/>
|
||||
<input type="text" name="username" placeholder="Username"/>
|
||||
|
@ -1,46 +0,0 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-list-users.js
|
||||
type: application/javascript
|
||||
module-type: command
|
||||
|
||||
Command to list users
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
exports.info = {
|
||||
name: "mws-list-users",
|
||||
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(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) {
|
||||
return "Error: MultiWikiServer or SQL database not initialized.";
|
||||
}
|
||||
|
||||
var users = $tw.mws.store.sqlTiddlerDatabase.listUsers().map(function(user){
|
||||
return ({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
created_at: user.created_at,
|
||||
})
|
||||
});
|
||||
console.log("Users:", users);
|
||||
self.callback(null, "Users retrieved successfully:\n" + JSON.stringify(users, null, 2));
|
||||
};
|
||||
|
||||
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,7 +35,7 @@ function Server(options) {
|
||||
this.authenticators = options.authenticators || [];
|
||||
this.wiki = options.wiki;
|
||||
this.boot = options.boot || $tw.boot;
|
||||
this.sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase;
|
||||
this.sqlTiddlerDatabase = options.sqlTiddlerDatabase || $tw.mws.store.sqlTiddlerDatabase;
|
||||
// Initialise the variables
|
||||
this.variables = $tw.utils.extend({},this.defaultVariables);
|
||||
if(options.variables) {
|
||||
@ -158,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) {
|
||||
@ -351,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
|
||||
*/
|
||||
@ -411,8 +420,7 @@ Server.prototype.redirectToLogin = function(response, returnUrl) {
|
||||
} else {
|
||||
console.log(`Invalid return URL detected: ${returnUrl}. Redirecting to home page.`);
|
||||
}
|
||||
response.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Path=/`);
|
||||
const loginUrl = '/login';
|
||||
response.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Secure; SameSite=Strict; Path=/`); const loginUrl = '/login';
|
||||
response.writeHead(302, {
|
||||
'Location': loginUrl
|
||||
});
|
||||
@ -468,6 +476,12 @@ Server.prototype.requestHandler = function(request,response,options) {
|
||||
|
||||
// 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") {
|
||||
|
@ -13,12 +13,14 @@ GET /bags/:bag_name
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware;
|
||||
|
||||
exports.method = "GET";
|
||||
|
||||
exports.path = /^\/bags\/([^\/]+)(\/?)$/;
|
||||
|
||||
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] !== "/") {
|
||||
@ -33,7 +35,6 @@ exports.handler = function (request, response, state) {
|
||||
if (request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
|
||||
state.sendResponse(200, { "Content-Type": "application/json" }, JSON.stringify(bagTiddlers), "utf8");
|
||||
} else {
|
||||
aclMiddleware(request, response, state, 'bag', 'READ');
|
||||
if (!response.headersSent) {
|
||||
// This is not a JSON API request, we should return the raw tiddler content
|
||||
response.writeHead(200, "OK", {
|
||||
|
@ -16,14 +16,15 @@ 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 = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "recipe"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
aclMiddleware(request, response, state, "recipe", "READ");
|
||||
// Get the parameters
|
||||
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
title = $tw.utils.decodeURIComponentSafe(state.params[1]),
|
||||
|
@ -12,8 +12,6 @@ POST /bags/:bag_name/tiddlers/
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/bags\/([^\/]+)\/tiddlers\/$/;
|
||||
@ -22,8 +20,11 @@ exports.bodyFormat = "stream";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "bag"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
aclMiddleware(request, response, state, "bag", "WRITE");
|
||||
const path = require("path"),
|
||||
fs = require("fs"),
|
||||
processIncomingStream = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/multipart-forms.js").processIncomingStream;
|
||||
|
@ -17,8 +17,6 @@ description
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/bags$/;
|
||||
@ -27,8 +25,11 @@ exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "bag"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
aclMiddleware(request, response, state, "bag", "WRITE");
|
||||
if(state.data.bag_name) {
|
||||
const result = $tw.mws.store.createBag(state.data.bag_name,state.data.description);
|
||||
if(!result) {
|
||||
|
@ -37,17 +37,29 @@ exports.handler = function(request,response,state) {
|
||||
var sessionId = auth.createSession(user.user_id);
|
||||
var returnUrl = state.server.parseCookieString(request.headers.cookie).returnUrl
|
||||
response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
|
||||
response.writeHead(302, {
|
||||
'Location': returnUrl || '/'
|
||||
});
|
||||
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"
|
||||
}));
|
||||
response.writeHead(302, {
|
||||
'Location': '/login'
|
||||
});
|
||||
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();
|
||||
};
|
||||
|
@ -18,8 +18,6 @@ bag_names: space separated list of bags
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "POST";
|
||||
|
||||
exports.path = /^\/recipes$/;
|
||||
@ -28,8 +26,11 @@ exports.bodyFormat = "www-form-urlencoded";
|
||||
|
||||
exports.csrfDisable = true;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "recipe"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
aclMiddleware(request, response, state, "recipe", "WRITE");
|
||||
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);
|
||||
if(!result) {
|
||||
|
@ -12,14 +12,15 @@ PUT /bags/:bag_name
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "PUT";
|
||||
|
||||
exports.path = /^\/bags\/(.+)$/;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "bag"
|
||||
|
||||
exports.handler = function(request,response,state) {
|
||||
aclMiddleware(request, response, state, "bag", "WRITE");
|
||||
// Get the parameters
|
||||
var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
data = $tw.utils.parseJSONSafe(state.data);
|
||||
|
@ -12,14 +12,15 @@ PUT /recipes/:recipe_name/tiddlers/:title
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "PUT";
|
||||
|
||||
exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "recipe"
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
aclMiddleware(request, response, state, "recipe", "WRITE");
|
||||
// Get the parameters
|
||||
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
title = $tw.utils.decodeURIComponentSafe(state.params[1]),
|
||||
|
@ -12,14 +12,15 @@ PUT /recipes/:recipe_name
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware;
|
||||
|
||||
exports.method = "PUT";
|
||||
|
||||
exports.path = /^\/recipes\/(.+)$/;
|
||||
|
||||
exports.useACL = true;
|
||||
|
||||
exports.entityName = "recipe"
|
||||
|
||||
exports.handler = function (request, response, state) {
|
||||
aclMiddleware(request, response, state, "recipe", "WRITE");
|
||||
// Get the parameters
|
||||
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
|
||||
data = $tw.utils.parseJSONSafe(state.data);
|
||||
|
@ -214,15 +214,13 @@ SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscon
|
||||
$accesscontrol: accesscontrol,
|
||||
$description: description
|
||||
});
|
||||
|
||||
|
||||
// update the permissions on ACL records
|
||||
const admin = this.getRoleByName('ADMIN');
|
||||
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);
|
||||
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;
|
||||
};
|
||||
@ -290,12 +288,12 @@ SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,descr
|
||||
|
||||
|
||||
// update the permissions on ACL records
|
||||
const admin = this.getRoleByName('ADMIN');
|
||||
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);
|
||||
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;
|
||||
};
|
||||
@ -495,34 +493,34 @@ 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)
|
||||
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)
|
||||
return this.checkACLPermission(userId, "bag", bagName, permissionName)
|
||||
};
|
||||
|
||||
SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName) {
|
||||
const entityTypeToTableMap = {
|
||||
bag: {
|
||||
table: 'bags',
|
||||
column: 'bag_name'
|
||||
table: "bags",
|
||||
column: "bag_name"
|
||||
},
|
||||
recipe: {
|
||||
table: 'recipes',
|
||||
column: 'recipe_name'
|
||||
table: "recipes",
|
||||
column: "recipe_name"
|
||||
}
|
||||
};
|
||||
|
||||
const entityInfo = entityTypeToTableMap[entityType];
|
||||
if (!entityInfo) {
|
||||
throw new Error('Invalid entity type: ' + entityType);
|
||||
throw new Error("Invalid entity type: " + entityType);
|
||||
}
|
||||
|
||||
// if the entityName starts with "$:/", we'll assume its a system tiddler, then grant the user permission
|
||||
// if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission
|
||||
if(entityName.startsWith("$:/")){
|
||||
return true
|
||||
}
|
||||
|
@ -5,43 +5,45 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
|
||||
<$transclude/>
|
||||
</$set>
|
||||
</$tiddler>
|
||||
|
||||
<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 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-roles">
|
||||
<h2>User Roles</h2>
|
||||
<ul>
|
||||
<$list filter="[<user-roles>jsonindexes[]]" variable="role-index">
|
||||
<li>
|
||||
<$text text={{{ [<user-roles>jsonextract<role-index>jsonget[role_name]] }}}/>
|
||||
</li>
|
||||
</$list>
|
||||
</ul>
|
||||
<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 Roles</h2>
|
||||
<ul>
|
||||
<$list filter="[<user-roles>jsonindexes[]]" variable="role-index">
|
||||
<li>
|
||||
<$text text={{{ [<user-roles>jsonextract<role-index>jsonget[role_name]] }}}/>
|
||||
</li>
|
||||
</$list>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<$reveal type="match" state="is-current-user-profile" text="yes">
|
||||
|
||||
<!-- <$reveal type="match" state="is-current-user-profile" text="yes"> -->
|
||||
<div class="user-profile-management">
|
||||
<h2>Manage Your Account</h2>
|
||||
<form class="user-profile-form">
|
||||
@ -53,28 +55,42 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" value={{{ [<user>jsonget[email]] }}} />
|
||||
</div>
|
||||
<button type="submit" class="update-profile-btn">Update Profile</button>
|
||||
</form>
|
||||
<hr />
|
||||
<h2>Danger Zone</h2>
|
||||
<form class="user-profile-form">
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password:</label>
|
||||
<input type="password" id="new-password" name="new-password" />
|
||||
<input type="password" id="new-password" name="new-password" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm New Password:</label>
|
||||
<input type="password" id="confirm-password" name="confirm-password" />
|
||||
<input type="password" id="confirm-password" name="confirm-password" required />
|
||||
</div>
|
||||
<button type="submit" class="update-profile-btn">Update Profile</button>
|
||||
<button type="submit" class="update-password-btn">Change Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</$reveal>
|
||||
<!-- </$reveal> -->
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
max-width: 80vw;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.user-profile-container {
|
||||
max-width: 600px;
|
||||
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 {
|
||||
@ -155,9 +171,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
|
||||
}
|
||||
|
||||
.user-profile-management {
|
||||
margin-top: 2rem;
|
||||
padding: 2rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.user-profile-management h2 {
|
||||
@ -166,6 +180,10 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-profile-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.user-profile-form .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@ -184,7 +202,8 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.update-profile-btn {
|
||||
.update-profile-btn,
|
||||
.update-password-btn {
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
border: none;
|
||||
@ -194,7 +213,15 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.update-password-btn {
|
||||
background: #00796b;
|
||||
}
|
||||
|
||||
.update-profile-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.update-password-btn:hover {
|
||||
background: #00695c;
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user