implemented adding users, prepared sending reset links
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -4,6 +4,7 @@ package de.srsoftware.oidc.api;
|
|||||||
import de.srsoftware.oidc.api.data.User;
|
import de.srsoftware.oidc.api.data.User;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public interface UserService {
|
public interface UserService {
|
||||||
/**
|
/**
|
||||||
@@ -22,6 +23,7 @@ public interface UserService {
|
|||||||
public Optional<User> forToken(String accessToken);
|
public Optional<User> forToken(String accessToken);
|
||||||
public UserService init(User defaultUser);
|
public UserService init(User defaultUser);
|
||||||
public List<User> list();
|
public List<User> list();
|
||||||
|
public Set<User> find(String key);
|
||||||
public Optional<User> load(String id);
|
public Optional<User> load(String id);
|
||||||
public Optional<User> load(String username, String password);
|
public Optional<User> load(String username, String password);
|
||||||
public boolean passwordMatches(String password, String hashedPassword);
|
public boolean passwordMatches(String password, String hashedPassword);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.api.data;
|
package de.srsoftware.oidc.api.data;
|
||||||
|
|
||||||
public enum Permission { MANAGE_CLIENTS }
|
public enum Permission { MANAGE_CLIENTS, MANAGE_USERS }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package de.srsoftware.oidc.api.data;
|
package de.srsoftware.oidc.api.data;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public final class User {
|
public final class User {
|
||||||
public static final String EMAIL = "email";
|
public static final String EMAIL = "email";
|
||||||
@@ -23,8 +24,8 @@ public final class User {
|
|||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User add(Permission permission) {
|
public User add(Permission... newPermissions) {
|
||||||
permissions.add(permission);
|
for (var permission : newPermissions) permissions.add(permission);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +69,21 @@ public final class User {
|
|||||||
return includePassword ? Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid, PASSWORD, hashedPassword) : Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid);
|
return includePassword ? Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid, PASSWORD, hashedPassword) : Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Optional<User> of(JSONObject json, String userId) {
|
||||||
|
var user = new User(json.getString(USERNAME), json.getString(PASSWORD), json.getString(REALNAME), json.getString(EMAIL), userId);
|
||||||
|
|
||||||
|
var perms = json.has(PERMISSIONS) ? json.getJSONArray(PERMISSIONS) : Set.of();
|
||||||
|
for (Object perm : perms) {
|
||||||
|
try {
|
||||||
|
if (perm instanceof String s) perm = Permission.valueOf(s);
|
||||||
|
if (perm instanceof Permission p) user.add(p);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.of(user);
|
||||||
|
}
|
||||||
|
|
||||||
public String realName() {
|
public String realName() {
|
||||||
return realName;
|
return realName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package de.srsoftware.oidc.app;
|
|||||||
|
|
||||||
import static de.srsoftware.oidc.api.Constants.*;
|
import static de.srsoftware.oidc.api.Constants.*;
|
||||||
import static de.srsoftware.oidc.api.data.Permission.MANAGE_CLIENTS;
|
import static de.srsoftware.oidc.api.data.Permission.MANAGE_CLIENTS;
|
||||||
|
import static de.srsoftware.oidc.api.data.Permission.MANAGE_USERS;
|
||||||
import static de.srsoftware.utils.Optionals.emptyIfBlank;
|
import static de.srsoftware.utils.Optionals.emptyIfBlank;
|
||||||
import static de.srsoftware.utils.Paths.configDir;
|
import static de.srsoftware.utils.Paths.configDir;
|
||||||
import static de.srsoftware.utils.Strings.uuid;
|
import static de.srsoftware.utils.Strings.uuid;
|
||||||
@@ -52,7 +53,7 @@ public class Application {
|
|||||||
var keyDir = storageFile.getParentFile().toPath().resolve("keys");
|
var keyDir = storageFile.getParentFile().toPath().resolve("keys");
|
||||||
var passwordHasher = new UuidHasher();
|
var passwordHasher = new UuidHasher();
|
||||||
var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID);
|
var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID);
|
||||||
var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS);
|
var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS, MANAGE_USERS);
|
||||||
KeyStorage keyStore = new PlaintextKeyStore(keyDir);
|
KeyStorage keyStore = new PlaintextKeyStore(keyDir);
|
||||||
KeyManager keyManager = new RotatingKeyManager(keyStore);
|
KeyManager keyManager = new RotatingKeyManager(keyStore);
|
||||||
FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser);
|
FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.backend;
|
package de.srsoftware.oidc.backend;
|
||||||
|
|
||||||
|
import static de.srsoftware.oidc.api.data.Permission.MANAGE_USERS;
|
||||||
import static de.srsoftware.oidc.api.data.User.*;
|
import static de.srsoftware.oidc.api.data.User.*;
|
||||||
|
import static de.srsoftware.utils.Strings.uuid;
|
||||||
|
import static java.lang.System.Logger.Level.WARNING;
|
||||||
import static java.net.HttpURLConnection.*;
|
import static java.net.HttpURLConnection.*;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
@@ -22,6 +25,15 @@ public class UserController extends Controller {
|
|||||||
users = userService;
|
users = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean addUser(HttpExchange ex, Session session) throws IOException {
|
||||||
|
var user = session.user();
|
||||||
|
if (!user.hasPermission(MANAGE_USERS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex);
|
||||||
|
var json = json(ex);
|
||||||
|
var newID = uuid();
|
||||||
|
User.of(json, uuid()).ifPresent(u -> users.updatePassword(u, json.getString(PASSWORD)));
|
||||||
|
return sendContent(ex, newID);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean doGet(String path, HttpExchange ex) throws IOException {
|
public boolean doGet(String path, HttpExchange ex) throws IOException {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
@@ -41,20 +53,14 @@ public class UserController extends Controller {
|
|||||||
return notFound(ex);
|
return notFound(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean userInfo(HttpExchange ex) throws IOException {
|
|
||||||
var optUser = getBearer(ex).flatMap(users::forToken);
|
|
||||||
if (optUser.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
|
|
||||||
var user = optUser.get();
|
|
||||||
var map = Map.of("sub", user.uuid(), "email", user.email());
|
|
||||||
return sendContent(ex, new JSONObject(map));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean doPost(String path, HttpExchange ex) throws IOException {
|
public boolean doPost(String path, HttpExchange ex) throws IOException {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case "/login":
|
case "/login":
|
||||||
return login(ex);
|
return login(ex);
|
||||||
|
case "/reset":
|
||||||
|
return resetPassword(ex);
|
||||||
}
|
}
|
||||||
var optSession = getSession(ex);
|
var optSession = getSession(ex);
|
||||||
if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
|
if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
|
||||||
@@ -64,6 +70,10 @@ public class UserController extends Controller {
|
|||||||
switch (path) {
|
switch (path) {
|
||||||
case "/":
|
case "/":
|
||||||
return sendUserAndCookie(ex, session);
|
return sendUserAndCookie(ex, session);
|
||||||
|
case "/add":
|
||||||
|
return addUser(ex, session);
|
||||||
|
case "/list":
|
||||||
|
return list(ex, session);
|
||||||
case "/password":
|
case "/password":
|
||||||
return updatePassword(ex, session);
|
return updatePassword(ex, session);
|
||||||
case "/update":
|
case "/update":
|
||||||
@@ -72,6 +82,14 @@ public class UserController extends Controller {
|
|||||||
return notFound(ex);
|
return notFound(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean list(HttpExchange ex, Session session) throws IOException {
|
||||||
|
var user = session.user();
|
||||||
|
if (!user.hasPermission(MANAGE_USERS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex);
|
||||||
|
var json = new JSONObject();
|
||||||
|
users.list().forEach(u -> json.put(u.uuid(), u.map(false)));
|
||||||
|
return sendContent(ex, json);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean login(HttpExchange ex) throws IOException {
|
private boolean login(HttpExchange ex) throws IOException {
|
||||||
var body = json(ex);
|
var body = json(ex);
|
||||||
|
|
||||||
@@ -89,6 +107,16 @@ public class UserController extends Controller {
|
|||||||
return sendEmptyResponse(HTTP_OK, ex);
|
return sendEmptyResponse(HTTP_OK, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean resetPassword(HttpExchange ex) throws IOException {
|
||||||
|
var idOrEmail = body(ex);
|
||||||
|
users.find(idOrEmail).forEach(this::senPasswordLink);
|
||||||
|
return sendEmptyResponse(HTTP_OK, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void senPasswordLink(User user) {
|
||||||
|
LOG.log(WARNING, "Sending password link to {0}", user.email());
|
||||||
|
}
|
||||||
|
|
||||||
private boolean sendUserAndCookie(HttpExchange ex, Session session) throws IOException {
|
private boolean sendUserAndCookie(HttpExchange ex, Session session) throws IOException {
|
||||||
new SessionToken(session.id()).addTo(ex);
|
new SessionToken(session.id()).addTo(ex);
|
||||||
return sendContent(ex, session.user().map(false));
|
return sendContent(ex, session.user().map(false));
|
||||||
@@ -122,7 +150,16 @@ public class UserController extends Controller {
|
|||||||
}
|
}
|
||||||
user.username(json.getString(USERNAME));
|
user.username(json.getString(USERNAME));
|
||||||
user.email(json.getString(EMAIL));
|
user.email(json.getString(EMAIL));
|
||||||
|
user.realName(json.getString(REALNAME));
|
||||||
users.save(user);
|
users.save(user);
|
||||||
return sendContent(ex, user.map(false));
|
return sendContent(ex, user.map(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean userInfo(HttpExchange ex) throws IOException {
|
||||||
|
var optUser = getBearer(ex).flatMap(users::forToken);
|
||||||
|
if (optUser.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
|
||||||
|
var user = optUser.get();
|
||||||
|
var map = Map.of("sub", user.uuid(), "email", user.email());
|
||||||
|
return sendContent(ex, new JSONObject(map));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
private static final String SESSIONS = "sessions";
|
private static final String SESSIONS = "sessions";
|
||||||
private static final String USERS = "users";
|
private static final String USERS = "users";
|
||||||
private static final String USER = "user";
|
private static final String USER = "user";
|
||||||
|
private static final List<String> KEYS = List.of(USERNAME, EMAIL, REALNAME);
|
||||||
|
|
||||||
private final Path storageFile;
|
private final Path storageFile;
|
||||||
private final JSONObject json;
|
private final JSONObject json;
|
||||||
@@ -91,10 +92,24 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<User> find(String key) {
|
||||||
|
var users = json.getJSONObject(USERS);
|
||||||
|
var result = new HashSet<User>();
|
||||||
|
for (var id : users.keySet()) {
|
||||||
|
var data = users.getJSONObject(id);
|
||||||
|
if (id.equals(key)) User.of(data, id).ifPresent(result::add);
|
||||||
|
if (KEYS.stream().map(data::getString).anyMatch(val -> val.equals(key))) User.of(data, id).ifPresent(result::add);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<User> list() {
|
public List<User> list() {
|
||||||
return List.of();
|
var users = json.getJSONObject(USERS);
|
||||||
|
List<User> result = new ArrayList<>();
|
||||||
|
for (var uid : users.keySet()) User.of(users.getJSONObject(uid), uid).ifPresent(result::add);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -103,24 +118,23 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
try {
|
try {
|
||||||
var users = json.getJSONObject(USERS);
|
var users = json.getJSONObject(USERS);
|
||||||
var userData = users.getJSONObject(userId);
|
var userData = users.getJSONObject(userId);
|
||||||
return userOf(userData, userId);
|
return User.of(userData, userId);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
return empty();
|
return empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> load(String username, String password) {
|
public Optional<User> load(String user, String password) {
|
||||||
try {
|
try {
|
||||||
var users = json.getJSONObject(USERS);
|
var users = json.getJSONObject(USERS);
|
||||||
var uuids = users.keySet();
|
var uuids = users.keySet();
|
||||||
for (String userId : uuids) {
|
for (String userId : uuids) {
|
||||||
var userData = users.getJSONObject(userId);
|
var userData = users.getJSONObject(userId);
|
||||||
if (!userData.getString(USERNAME).equals(username)) continue;
|
|
||||||
|
if (KEYS.stream().map(userData::getString).noneMatch(val -> val.equals(user))) continue;
|
||||||
var hashedPass = userData.getString(PASSWORD);
|
var hashedPass = userData.getString(PASSWORD);
|
||||||
if (passwordHasher.matches(password, hashedPass)) {
|
if (passwordHasher.matches(password, hashedPass)) return User.of(userData, userId);
|
||||||
return userOf(userData, userId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return empty();
|
return empty();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -141,34 +155,16 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
} else {
|
} else {
|
||||||
users = json.getJSONObject(USERS);
|
users = json.getJSONObject(USERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
users.put(user.uuid(), user.map(true));
|
users.put(user.uuid(), user.map(true));
|
||||||
return save();
|
return save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FileStore updatePassword(User user, String plaintextPassword) {
|
public FileStore updatePassword(User user, String plaintextPassword) {
|
||||||
var oldHashedPassword = user.hashedPassword();
|
return save(user.hashedPassword(passwordHasher.hash(plaintextPassword, uuid())));
|
||||||
var salt = passwordHasher.salt(oldHashedPassword);
|
|
||||||
user.hashedPassword(passwordHasher.hash(plaintextPassword, salt));
|
|
||||||
return save(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<User> userOf(JSONObject json, String userId) {
|
|
||||||
var user = new User(json.getString(USERNAME), json.getString(PASSWORD), json.getString(REALNAME), json.getString(EMAIL), userId);
|
|
||||||
var perms = json.getJSONArray(PERMISSIONS);
|
|
||||||
for (Object perm : perms) {
|
|
||||||
try {
|
|
||||||
if (perm instanceof String s) perm = Permission.valueOf(s);
|
|
||||||
if (perm instanceof Permission p) user.add(p);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.of(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*** Session Service Methods ***/
|
/*** Session Service Methods ***/
|
||||||
|
|
||||||
// TODO: prolong session on user activity
|
// TODO: prolong session on user activity
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<a href="clients.html" class="MANAGE_CLIENTS">Clients</a>
|
<a href="clients.html" class="MANAGE_CLIENTS">Clients</a>
|
||||||
<a href="users.html" class="MANAGE_USERS">Benutzer</a>
|
<a href="users.html" class="MANAGE_USERS">Benutzer</a>
|
||||||
<a href="settings.html">Einstellungen</a>
|
<a href="settings.html">Einstellungen</a>
|
||||||
|
<a href="todo.html">TODO</a>
|
||||||
<a href="logout.html">Abmelden</a>
|
<a href="logout.html">Abmelden</a>
|
||||||
@@ -21,6 +21,10 @@
|
|||||||
<th>Benutzername</th>
|
<th>Benutzername</th>
|
||||||
<td><input id="username" type="text"></td>
|
<td><input id="username" type="text"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Anzeigename</th>
|
||||||
|
<td><input id="realname" type="text"></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>E-Mail</th>
|
<th>E-Mail</th>
|
||||||
<td><input id="email" type="email"></td>
|
<td><input id="email" type="email"></td>
|
||||||
@@ -48,6 +52,10 @@
|
|||||||
<th>altes Passwort</th>
|
<th>altes Passwort</th>
|
||||||
<td><input id="oldpass" type="password"></td>
|
<td><input id="oldpass" type="password"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<td><input type="text" style="visibility: hidden"></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Neues Passwort</th>
|
<th>Neues Passwort</th>
|
||||||
<td><input id="newpass1" type="password"></td>
|
<td><input id="newpass1" type="password"></td>
|
||||||
@@ -14,7 +14,10 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User name</th>
|
<th>User name</th>
|
||||||
<td><input type="text" id="username" placeholder="User name *"/></td>
|
<td style="position: relative">
|
||||||
|
<input type="text" id="username" placeholder="User name *"/>
|
||||||
|
<span id="bubble" style="display: none">Please enter a username or email to reset your password!</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Password</th>
|
<th>Password</th>
|
||||||
@@ -28,8 +31,15 @@
|
|||||||
<td></td>
|
<td></td>
|
||||||
<td><button type="button" onClick="tryLogin()">Login</button></td>
|
<td><button type="button" onClick="tryLogin()">Login</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><button type="button" class="light" onClick="resetPw()">reset password?</button></td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<div id="sent" class="warning" style="display: none">
|
||||||
|
A link to reset your password has been sent.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="ad">
|
<div id="ad">
|
||||||
This tiny login provider is brought to you by <b>SRSoftware</b>. <a href="https://git.srsoftware.de/StephanRichter/LightOidc">Find out more…</a>
|
This tiny login provider is brought to you by <b>SRSoftware</b>. <a href="https://git.srsoftware.de/StephanRichter/LightOidc">Find out more…</a>
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<a href="clients.html" class="MANAGE_CLIENTS">Clients</a>
|
<a href="clients.html" class="MANAGE_CLIENTS">Clients</a>
|
||||||
<a href="users.html" class="MANAGE_USERS">Users</a>
|
<a href="users.html" class="MANAGE_USERS">Users</a>
|
||||||
<a href="settings.html">Settings</a>
|
<a href="settings.html">Settings</a>
|
||||||
|
<a href="todo.html">TODO</a>
|
||||||
<a href="logout.html">Logout</a>
|
<a href="logout.html">Logout</a>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
function edit(clientId){
|
||||||
|
redirect("edit_client.html?id="+clientId);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleClients(response){
|
async function handleClients(response){
|
||||||
if (response.status == UNAUTHORIZED) {
|
if (response.status == UNAUTHORIZED) {
|
||||||
redirect('login.html?return_to='+encodeURI(window.location.href))
|
redirect('login.html?return_to='+encodeURI(window.location.href))
|
||||||
@@ -8,7 +12,13 @@ async function handleClients(response){
|
|||||||
for (let id in clients){
|
for (let id in clients){
|
||||||
var row = document.createElement("tr");
|
var row = document.createElement("tr");
|
||||||
var client = clients[id];
|
var client = clients[id];
|
||||||
row.innerHTML = "<td>"+client.name+"</td>\n<td>"+id+"</td>\n<td>"+client.redirect_uris.join("<br/>")+'</td>\n<td><button type="button" onclick="edit(\''+id+'\')">Edit</button><button class="danger" onclick="remove(\''+id+'\')" type="button">Remove</button></td>';
|
row.innerHTML = `<td>${client.name}</td>
|
||||||
|
<td>${id}</td>
|
||||||
|
<td>${client.redirect_uris.join("<br/>")}</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" onclick="edit('${id}')">Edit</button>
|
||||||
|
<button class="danger" onclick="remove('${id}')" type="button">Remove</button>
|
||||||
|
</td>`;
|
||||||
bottom.parentNode.insertBefore(row,bottom);
|
bottom.parentNode.insertBefore(row,bottom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,8 +37,6 @@ function remove(clientId){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(clientId){
|
|
||||||
redirect("edit_client.html?id="+clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(client_controller+"/list",{method:'POST'}).then(handleClients);
|
fetch(client_controller+"/list",{method:'POST'}).then(handleClients);
|
||||||
@@ -10,7 +10,7 @@ function get(id){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function disable(id){
|
function disable(id){
|
||||||
get(id).setAttribute('disabled',true);
|
get(id).setAttribute('disabled','disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
function enable(id){
|
function enable(id){
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
function doRedirect(){
|
||||||
|
let params = new URL(document.location.toString()).searchParams;
|
||||||
|
redirect( params.get("return_to") || 'index.html');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogin(response){
|
async function handleLogin(response){
|
||||||
if (response.ok){
|
if (response.ok){
|
||||||
var body = await response.json();
|
var body = await response.json();
|
||||||
@@ -8,10 +14,24 @@ async function handleLogin(response){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doRedirect(){
|
function keyDown(ev){
|
||||||
let params = new URL(document.location.toString()).searchParams;
|
if (event.keyCode == 13) tryLogin();
|
||||||
redirect( params.get("return_to") || 'index.html');
|
}
|
||||||
return false;
|
|
||||||
|
function resetPw(){
|
||||||
|
var user = getValue('username');
|
||||||
|
if (!user) {
|
||||||
|
show('bubble');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hide('bubble');
|
||||||
|
fetch(user_controller+"/reset",{
|
||||||
|
method: 'POST',
|
||||||
|
body:user
|
||||||
|
}).then(() => {
|
||||||
|
hide('login');
|
||||||
|
show('sent');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryLogin(){
|
function tryLogin(){
|
||||||
@@ -29,8 +49,4 @@ function tryLogin(){
|
|||||||
})
|
})
|
||||||
}).then(handleLogin);
|
}).then(handleLogin);
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
function keyDown(ev){
|
|
||||||
if (event.keyCode == 13) tryLogin();
|
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ function fillForm(){
|
|||||||
setValue('username',user.username);
|
setValue('username',user.username);
|
||||||
setValue('email',user.email);
|
setValue('email',user.email);
|
||||||
setValue('uuid', user.uuid);
|
setValue('uuid', user.uuid);
|
||||||
|
setValue('realname', user.realname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ function update(){
|
|||||||
var newData = {
|
var newData = {
|
||||||
username : getValue('username'),
|
username : getValue('username'),
|
||||||
email : getValue('email'),
|
email : getValue('email'),
|
||||||
|
realname : getValue('realname'),
|
||||||
uuid : getValue('uuid')
|
uuid : getValue('uuid')
|
||||||
}
|
}
|
||||||
fetch(user_controller+'/update',{
|
fetch(user_controller+'/update',{
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ async function handleNavigation(response){
|
|||||||
var nav = document.getElementsByTagName('nav')[0];
|
var nav = document.getElementsByTagName('nav')[0];
|
||||||
nav.innerHTML = content;
|
nav.innerHTML = content;
|
||||||
var links = nav.getElementsByTagName('a');
|
var links = nav.getElementsByTagName('a');
|
||||||
for (var index = 0; index < links.length; index++){
|
for (var index = links.length; index > 0; index--){
|
||||||
var link = links[index];
|
var link = links[index-1];
|
||||||
var clazz = link.hasAttribute('class') ? link.getAttribute("class") : null;
|
var clazz = link.hasAttribute('class') ? link.getAttribute("class") : null;
|
||||||
if (clazz != null && !user.permissions.includes(clazz)) nav.removeChild(link);
|
if (clazz != null && !user.permissions.includes(clazz)) nav.removeChild(link);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
function addUser(){
|
||||||
|
var pw = getValue('pw');
|
||||||
|
var pw2 = getValue('pw2');
|
||||||
|
if (pw != pw2) {
|
||||||
|
show('pw-mismatch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var msg = {
|
||||||
|
username : getValue('username'),
|
||||||
|
realname : getValue('realname'),
|
||||||
|
email : getValue('email'),
|
||||||
|
password : pw
|
||||||
|
};
|
||||||
|
fetch(user_controller+"/add",{
|
||||||
|
method:'POST',
|
||||||
|
header: {
|
||||||
|
'Content-Type':'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(msg)
|
||||||
|
}).then(() => location.reload())
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUsers(response){
|
||||||
|
if (response.status == UNAUTHORIZED) {
|
||||||
|
redirect('login.html?return_to='+encodeURI(window.location.href))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var users = await response.json();
|
||||||
|
var bottom = document.getElementById('bottom');
|
||||||
|
for (let id in users){
|
||||||
|
var row = document.createElement("tr");
|
||||||
|
var user = users[id];
|
||||||
|
row.innerHTML = `<td>${user.username}</td>
|
||||||
|
<td>${user.realname}</td>
|
||||||
|
<td>${user.email}</td>
|
||||||
|
<td>${id}</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" onclick="reset_password('${id}')" id="reset-${id}">Reset password</button>
|
||||||
|
<button class="danger" onclick="remove('${id}')" type="button">Remove</button>
|
||||||
|
</td>`;
|
||||||
|
bottom.parentNode.insertBefore(row,bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemove(response){
|
||||||
|
redirect("users.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(userId){
|
||||||
|
var message = document.getElementById('message').innerHTML;
|
||||||
|
if (confirm(message.replace("{}",userId))) {
|
||||||
|
fetch(user_controller+"/delete",{
|
||||||
|
method: 'DELETE',
|
||||||
|
body : JSON.stringify({ userId : userId })
|
||||||
|
}).then(handleRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset_password(userid){
|
||||||
|
fetch(user_controller+"/reset",{
|
||||||
|
method: 'POST',
|
||||||
|
body:userid
|
||||||
|
}).then(() => {
|
||||||
|
disable('reset-'+userid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(user_controller+"/list",{method:'POST'}).then(handleUsers);
|
||||||
@@ -21,6 +21,10 @@
|
|||||||
<th>User name</th>
|
<th>User name</th>
|
||||||
<td><input id="username" type="text"></td>
|
<td><input id="username" type="text"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Display name</th>
|
||||||
|
<td><input id="realname" type="text"></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<td><input id="email" type="email"></td>
|
<td><input id="email" type="email"></td>
|
||||||
@@ -48,6 +52,10 @@
|
|||||||
<th>Old password</th>
|
<th>Old password</th>
|
||||||
<td><input id="oldpass" type="password"></td>
|
<td><input id="oldpass" type="password"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<td><input type="text" style="visibility: hidden"></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>New Password</th>
|
<th>New Password</th>
|
||||||
<td><input id="newpass1" type="password"></td>
|
<td><input id="newpass1" type="password"></td>
|
||||||
|
|||||||
@@ -29,6 +29,30 @@ button {
|
|||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:hover{
|
||||||
|
padding: 9px;
|
||||||
|
border: 1px solid orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[disabled]{
|
||||||
|
background: rgb(48,48,48);
|
||||||
|
border: 1px solid gray;
|
||||||
|
color: gray;
|
||||||
|
padding: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.light{
|
||||||
|
background: rgb(48,48,48);
|
||||||
|
color: #444;
|
||||||
|
border: 1px solid #444;
|
||||||
|
padding: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.light:hover{
|
||||||
|
background: indigo;
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
#login button{
|
#login button{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -72,8 +96,10 @@ form th{
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset .centered th,
|
.centered,
|
||||||
form .centered th{
|
.centered input,
|
||||||
|
.centered th,
|
||||||
|
.centered th{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,4 +127,34 @@ legend {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
ul{
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bubble {
|
||||||
|
--c: indigo;
|
||||||
|
background: var(--c);
|
||||||
|
color: orange;
|
||||||
|
padding: .5em 1em;
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 100%;
|
||||||
|
width: 220px;
|
||||||
|
|
||||||
|
border-radius: 0 .25em .25em .25em;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
border: .5em solid transparent;
|
||||||
|
border-top-color: var(--c);
|
||||||
|
border-right-color: var(--c);
|
||||||
|
right: 100%;
|
||||||
|
top: 0%;
|
||||||
|
|
||||||
|
// magic
|
||||||
|
transform: skewY(20deg);
|
||||||
|
transform-origin: 100% 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
22
de.srsoftware.oidc.web/src/main/resources/en/todo.html
Normal file
22
de.srsoftware.oidc.web/src/main/resources/en/todo.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Light OIDC</title>
|
||||||
|
<script src="scripts/common.js"></script>
|
||||||
|
<script src="scripts/user.js"></script>
|
||||||
|
<script src="scripts/users.js"></script>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav></nav>
|
||||||
|
<div id="content">
|
||||||
|
<h1>to do…</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="users.html">Users: remove</a></li>
|
||||||
|
<li><a href="users.html">Users: send password reset link</a></li>
|
||||||
|
<li><a href="login.html">Login: send password reset link</a></li>
|
||||||
|
<li><a href="login.html">Login: "remember me" option</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
48
de.srsoftware.oidc.web/src/main/resources/en/users.html
Normal file
48
de.srsoftware.oidc.web/src/main/resources/en/users.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Light OIDC</title>
|
||||||
|
<script src="scripts/common.js"></script>
|
||||||
|
<script src="scripts/user.js"></script>
|
||||||
|
<script src="scripts/users.js"></script>
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav></nav>
|
||||||
|
<div id="content">
|
||||||
|
<h1>Users</h1>
|
||||||
|
<fieldset>
|
||||||
|
<legend>These are users that are registered with LightOIDC:</legend>
|
||||||
|
<table class="centered">
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Display name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
<tr id="bottom">
|
||||||
|
<td>
|
||||||
|
<input value="" type="text" id="username" placeholder="user name" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input value="" type="text" id="realname" placeholder="display name" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input value="" type="text" id="email" placeholder="email address" autocomplete="off" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input value="" type="password" id="pw" placeholder="password" autocomplete="new-password" />
|
||||||
|
<input value="" type="password" id="pw2" placeholder="repeat password" autocomplete="repeat-password" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button onclick="addUser()">Add new user…</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
<span class="error" style="display: none" id="pw-mismatch">Passwords do not match!</span>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user