mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-07-04 11:02:51 +00:00
refactor auth routes & added user management page
This commit is contained in:
parent
4e753603e8
commit
0932f2ce79
@ -4,8 +4,7 @@
|
|||||||
"tiddlywiki/tiddlyweb",
|
"tiddlywiki/tiddlyweb",
|
||||||
"tiddlywiki/filesystem",
|
"tiddlywiki/filesystem",
|
||||||
"tiddlywiki/multiwikiclient",
|
"tiddlywiki/multiwikiclient",
|
||||||
"tiddlywiki/multiwikiserver",
|
"tiddlywiki/multiwikiserver"
|
||||||
"tiddlywiki/authentication"
|
|
||||||
],
|
],
|
||||||
"themes": [
|
"themes": [
|
||||||
"tiddlywiki/vanilla",
|
"tiddlywiki/vanilla",
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "$:/plugins/tiddlywiki/authentication",
|
|
||||||
"description": "Authentication plugin for TiddlyWiki",
|
|
||||||
"author": "Anon",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"core-version": ">=5.0.0",
|
|
||||||
"plugin-type": "plugin",
|
|
||||||
"list": ["login"]
|
|
||||||
}
|
|
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>
|
10
plugins/tiddlywiki/multiwikiserver/auth/form/login/form.tid
Normal file
10
plugins/tiddlywiki/multiwikiserver/auth/form/login/form.tid
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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"/>
|
||||||
|
<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>
|
@ -1,19 +1,5 @@
|
|||||||
title: $:/plugins/tiddlywiki/authentication/login
|
title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/styles
|
||||||
tags: $:/tags/ServerRoute
|
|
||||||
route-method: GET
|
|
||||||
route-path: /login
|
|
||||||
|
|
||||||
\define loginForm()
|
|
||||||
<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>
|
|
||||||
\end
|
|
||||||
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
@ -59,19 +45,4 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-container">
|
|
||||||
<h1>TiddlyWiki Login</h1>
|
|
||||||
<$set name="returnUrl" value={{{ [{$:/temp/mws/login/returnUrl}!is[blank]else{$:/info/url/query}split[returnUrl=]last[]else[/]] }}}>
|
|
||||||
<<loginForm>>
|
|
||||||
</$set>
|
|
||||||
<$list filter="[[$:/temp/mws/login/error]!is[missing]]" variable="errorTiddler">
|
|
||||||
<div class="tc-error-message">
|
|
||||||
{{$:/temp/mws/login/error}}
|
|
||||||
</div>
|
|
||||||
</$list>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -77,8 +77,6 @@ function Server(options) {
|
|||||||
$tw.modules.forEachModuleOfType("mws-route", function(title,routeDefinition) {
|
$tw.modules.forEachModuleOfType("mws-route", function(title,routeDefinition) {
|
||||||
self.addRoute(routeDefinition);
|
self.addRoute(routeDefinition);
|
||||||
});
|
});
|
||||||
// Load tiddler-based routes
|
|
||||||
self.loadAuthRoutes();
|
|
||||||
// Initialise the http vs https
|
// Initialise the http vs https
|
||||||
this.listenOptions = null;
|
this.listenOptions = null;
|
||||||
this.protocol = "http";
|
this.protocol = "http";
|
||||||
@ -304,88 +302,6 @@ Server.prototype.addRoute = function(route) {
|
|||||||
this.routes.push(route);
|
this.routes.push(route);
|
||||||
};
|
};
|
||||||
|
|
||||||
Server.prototype.loadAuthRoutes = function () {
|
|
||||||
var self = this;
|
|
||||||
// add the login page route
|
|
||||||
self.addRoute({
|
|
||||||
method: "GET",
|
|
||||||
path: /^\/login$/,
|
|
||||||
handler: function (request, response, state) {
|
|
||||||
// Check if the user already has a valid session
|
|
||||||
const authenticatedUser = self.authenticateUser(request, response);
|
|
||||||
if (authenticatedUser) {
|
|
||||||
// User is already logged in, redirect to home page
|
|
||||||
response.writeHead(302, { 'Location': '/' });
|
|
||||||
response.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var loginTiddler = self.wiki.getTiddler("$:/plugins/tiddlywiki/authentication/login");
|
|
||||||
if (loginTiddler) {
|
|
||||||
var text = self.wiki.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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// add the login submission handler route
|
|
||||||
self.addRoute({
|
|
||||||
method: "POST",
|
|
||||||
path: /^\/login$/,
|
|
||||||
csrfDisable: true,
|
|
||||||
handler: function(request, response, state) {
|
|
||||||
self.handleLogin(request, response, state);
|
|
||||||
}.bind(self)
|
|
||||||
});
|
|
||||||
self.addRoute({
|
|
||||||
method: "POST",
|
|
||||||
path: /^\/logout$/,
|
|
||||||
csrfDisable: true,
|
|
||||||
handler: function(request, response, state) {
|
|
||||||
self.handleLogout(request, response, state);
|
|
||||||
}.bind(self)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Server.prototype.handleLogout = function (request, response, state) {
|
|
||||||
var self = this;
|
|
||||||
if (state.authenticatedUser) {
|
|
||||||
self.sqlTiddlerDatabase.deleteSession(state.authenticatedUser.sessionId);
|
|
||||||
}
|
|
||||||
response.setHeader('Set-Cookie', 'session=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT');
|
|
||||||
response.writeHead(302, { 'Location': '/login' });
|
|
||||||
response.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype.handleLogin = function (request, response, state) {
|
|
||||||
var self = this;
|
|
||||||
const querystring = require('querystring');
|
|
||||||
const formData = querystring.parse(state.data);
|
|
||||||
const { username, password } = formData;
|
|
||||||
const user = self.sqlTiddlerDatabase.getUserByUsername(username);
|
|
||||||
const isPasswordValid = self.verifyPassword(password, user?.password)
|
|
||||||
|
|
||||||
if (user && isPasswordValid) {
|
|
||||||
const sessionId = self.createSession(user.user_id);
|
|
||||||
const {returnUrl} = this.parseCookieString(request.headers.cookie)
|
|
||||||
response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
|
|
||||||
response.writeHead(302, {
|
|
||||||
'Location': returnUrl || '/'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.wiki.addTiddler(new $tw.Tiddler({
|
|
||||||
title: "$:/temp/mws/login/error",
|
|
||||||
text: "Invalid username or password"
|
|
||||||
}));
|
|
||||||
response.writeHead(302, {
|
|
||||||
'Location': '/login'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
response.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
Server.prototype.verifyPassword = function(inputPassword, storedHash) {
|
Server.prototype.verifyPassword = function(inputPassword, storedHash) {
|
||||||
const hashedInput = this.hashPassword(inputPassword);
|
const hashedInput = this.hashPassword(inputPassword);
|
||||||
return hashedInput === storedHash;
|
return hashedInput === storedHash;
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}());
|
@ -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() : ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log("Processed userList =>", userList);
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
response.write(html);
|
||||||
|
response.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
}());
|
@ -0,0 +1,53 @@
|
|||||||
|
/*\
|
||||||
|
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";
|
||||||
|
|
||||||
|
exports.method = "POST";
|
||||||
|
|
||||||
|
exports.path = /^\/login$/;
|
||||||
|
|
||||||
|
exports.bodyFormat = "www-form-urlencoded";
|
||||||
|
|
||||||
|
exports.csrfDisable = true;
|
||||||
|
|
||||||
|
exports.handler = function(request,response,state) {
|
||||||
|
var username = state.data.username;
|
||||||
|
var password = state.data.password;
|
||||||
|
var user = state.server.sqlTiddlerDatabase.getUserByUsername(username);
|
||||||
|
var isPasswordValid = state.server.verifyPassword(password, user ? user.password : null)
|
||||||
|
|
||||||
|
if(user && isPasswordValid) {
|
||||||
|
var sessionId = state.server.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 || '/'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({
|
||||||
|
title: "$:/temp/mws/login/error",
|
||||||
|
text: "Invalid username or password"
|
||||||
|
}));
|
||||||
|
response.writeHead(302, {
|
||||||
|
'Location': '/login'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
response.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
}());
|
@ -0,0 +1,30 @@
|
|||||||
|
/*\
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
response.setHeader("Set-Cookie", "session=; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
||||||
|
response.writeHead(302, { "Location": "/login" });
|
||||||
|
response.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
}());
|
89
plugins/tiddlywiki/multiwikiserver/templates/get-users.tid
Normal file
89
plugins/tiddlywiki/multiwikiserver/templates/get-users.tid
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!--
|
||||||
|
title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-users
|
||||||
|
-->
|
||||||
|
|
||||||
|
\define lingo-base() $:/language/ControlPanel/Tools/
|
||||||
|
|
||||||
|
<h1>User Management</h1>
|
||||||
|
|
||||||
|
<!-- Display raw user-list for debugging -->
|
||||||
|
<h2>Debug Info</h2>
|
||||||
|
<p><strong>Raw user list JSON:</strong> <$text text=<<user-list>>/></p>
|
||||||
|
|
||||||
|
<!-- Attempt to parse the user-list JSON -->
|
||||||
|
<$set name="userList" value=<<user-list>>>
|
||||||
|
<!-- Display a message if the userList is empty -->
|
||||||
|
<$list filter="[<userList>!is[blank]]" emptyMessage="The user list is empty or not provided.">
|
||||||
|
<p><strong>User list found:</strong> <$text text=<<userList>>/></p>
|
||||||
|
|
||||||
|
<!-- Attempt to parse the user list -->
|
||||||
|
<$set name="parsedUserList" value={{{ [<userList>jsonparse[]] }}}/>
|
||||||
|
|
||||||
|
<!-- Check if parsedUserList has any entries -->
|
||||||
|
<$list filter="[<parsedUserList>count[]compare:number:gt[0]]" emptyMessage="No users found or failed to parse user data">
|
||||||
|
<!-- Display parsed user list for debugging -->
|
||||||
|
<p><strong>Parsed User List (as JSON):</strong> <$text text={{{ [<parsedUserList>jsonstringify[]] }}}/></p>
|
||||||
|
|
||||||
|
<!-- Display total user count -->
|
||||||
|
<p><strong>Total users:</strong> <$text text={{{ [<parsedUserList>count[]] }}}/></p>
|
||||||
|
|
||||||
|
<!-- Render the user table if parsing was successful -->
|
||||||
|
<table class="tc-view-field-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Last Login</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<$list filter="[<userList>jsonindexes[]]" variable="userIndex">
|
||||||
|
<$let currentUser={{{ [<userList>jsonget<userIndex>] }}}>
|
||||||
|
<tr>
|
||||||
|
<td><$text text={{{ [<currentUser>jsonget[username]] }}}/></td>
|
||||||
|
<td><$text text={{{ [<currentUser>jsonget[email]] }}}/></td>
|
||||||
|
<td><$text text={{{ [<currentUser>jsonget[created_at]] }}}/></td>
|
||||||
|
<td><$text text={{{ [<currentUser>jsonget[last_login]] }}}/></td>
|
||||||
|
<td>
|
||||||
|
<$button message="tm-server-command" param="edit-user" user_id={{{ [<currentUser>jsonget[user_id]] }}} class="tc-btn-invisible">
|
||||||
|
{{$:/core/images/edit-button}} Edit
|
||||||
|
</$button>
|
||||||
|
<$button message="tm-server-command" param="delete-user" user_id={{{ [<currentUser>jsonget[user_id]] }}} class="tc-btn-invisible">
|
||||||
|
{{$:/core/images/delete-button}} Delete
|
||||||
|
</$button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</$let>
|
||||||
|
</$list>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</$list>
|
||||||
|
</$list>
|
||||||
|
</$set>
|
||||||
|
|
||||||
|
<$button message="tm-modal" param="$:/plugins/tiddlywiki/multiwikiserver/templates/add-user-modal" class="tc-btn-big-green">
|
||||||
|
Add New User
|
||||||
|
</$button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tc-view-field-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-view-field-table th, .tc-view-field-table td {
|
||||||
|
border: 1px solid <<colour table-border>>;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-view-field-table th {
|
||||||
|
background-color: <<colour table-header-background>>;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tc-view-field-table tr:nth-child(even) {
|
||||||
|
background-color: <<colour table-header-background>>;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
x
Reference in New Issue
Block a user