frontend: add auth overlays
This commit is contained in:
parent
706372716e
commit
82926ab172
10 changed files with 232 additions and 17 deletions
|
@ -115,6 +115,10 @@ body {
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-backdrop-opaque {
|
||||||
|
background-color: var(--background-color-1);
|
||||||
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
22
frontend/src/auth.js
Normal file
22
frontend/src/auth.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import gateway, { GatewayEventType } from "./gateway";
|
||||||
|
import { setAuthToken } from "./storage";
|
||||||
|
import { overlayStore } from "./stores";
|
||||||
|
|
||||||
|
function useAuthHandlers() {
|
||||||
|
gateway.subscribe(GatewayEventType.Ready, () => {
|
||||||
|
overlayStore.close("login");
|
||||||
|
overlayStore.close("createAccount");
|
||||||
|
});
|
||||||
|
|
||||||
|
gateway.subscribe(GatewayEventType.BadAuth, () => {
|
||||||
|
overlayStore.open("login", {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authWithToken(token, shouldUpdate=false) {
|
||||||
|
if (shouldUpdate)
|
||||||
|
setAuthToken(token);
|
||||||
|
gateway.init(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
useAuthHandlers();
|
77
frontend/src/components/overlays/CreateAccount.svelte
Normal file
77
frontend/src/components/overlays/CreateAccount.svelte
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<script>
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
import { quintInOut } from "svelte/easing";
|
||||||
|
import { overlayStore } from "../../stores";
|
||||||
|
import request from "../../request";
|
||||||
|
import { apiRoute } from "../../storage";
|
||||||
|
|
||||||
|
let username = "";
|
||||||
|
let password = "";
|
||||||
|
let buttonsEnabled = true;
|
||||||
|
let pendingOtherOpen = false;
|
||||||
|
|
||||||
|
const close = () => overlayStore.close('createAccount');
|
||||||
|
const create = async () => {
|
||||||
|
buttonsEnabled = false;
|
||||||
|
const { ok } = await request("POST", apiRoute("users/register"), false, {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
if (ok) {
|
||||||
|
overlayStore.open("toast", {
|
||||||
|
message: "Account created"
|
||||||
|
});
|
||||||
|
loginInstead();
|
||||||
|
} else {
|
||||||
|
overlayStore.open("toast", {
|
||||||
|
message: "Couldn't create account"
|
||||||
|
});
|
||||||
|
buttonsEnabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const loginInstead = () => {
|
||||||
|
close();
|
||||||
|
pendingOtherOpen = true;
|
||||||
|
}
|
||||||
|
const outroEnd = () => {
|
||||||
|
if (pendingOtherOpen) {
|
||||||
|
overlayStore.open("login", {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="modal-backdrop modal-backdrop-opaque">
|
||||||
|
<div class="modal" transition:fly="{{ duration: 500, easing: quintInOut, y: 10 }}" on:click|stopPropagation on:outroend="{ outroEnd }">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="h4">Create an Account</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="input-label">
|
||||||
|
Username
|
||||||
|
<input class="input full-width" minlength="1" maxlength="32" bind:value={ username } />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="separator" />
|
||||||
|
|
||||||
|
<label class="input-label">
|
||||||
|
Password
|
||||||
|
<input class="input full-width" minlength="8" type="password" bind:value={ password } />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="button modal-secondary-action" on:click="{ loginInstead }">Log in instead</button>
|
||||||
|
<button class="button button-accent modal-primary-action" on:click="{ create }" disabled="{ !buttonsEnabled }">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
81
frontend/src/components/overlays/Login.svelte
Normal file
81
frontend/src/components/overlays/Login.svelte
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<script>
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
import { quintInOut } from "svelte/easing";
|
||||||
|
import { overlayStore } from "../../stores";
|
||||||
|
import request from "../../request";
|
||||||
|
import { apiRoute } from "../../storage";
|
||||||
|
import { authWithToken } from "../../auth";
|
||||||
|
|
||||||
|
let username = "";
|
||||||
|
let password = "";
|
||||||
|
let buttonsEnabled = true;
|
||||||
|
let pendingOtherOpen = false;
|
||||||
|
|
||||||
|
const close = () => overlayStore.close('login');
|
||||||
|
const login = async () => {
|
||||||
|
buttonsEnabled = false;
|
||||||
|
const { ok, json } = await request("POST", apiRoute("users/login"), false, {
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
if (ok && json && json.token) {
|
||||||
|
authWithToken(json.token, true);
|
||||||
|
} else {
|
||||||
|
if (json && json.code && json.code === 6002) { // 6002 is the code for bad login
|
||||||
|
overlayStore.open("toast", {
|
||||||
|
message: "Invalid username or password"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
overlayStore.open("toast", {
|
||||||
|
message: "Couldn't log in"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
buttonsEnabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const createAccountInstead = () => {
|
||||||
|
close();
|
||||||
|
pendingOtherOpen = true;
|
||||||
|
};
|
||||||
|
const outroEnd = () => {
|
||||||
|
if (pendingOtherOpen) {
|
||||||
|
overlayStore.open("createAccount", {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="modal-backdrop modal-backdrop-opaque">
|
||||||
|
<div class="modal" transition:fly="{{ duration: 500, easing: quintInOut, y: 10 }}" on:click|stopPropagation on:outroend="{ outroEnd }">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="h4">Welcome back!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="input-label">
|
||||||
|
Username
|
||||||
|
<input class="input full-width" minlength="1" maxlength="32" bind:value={ username } />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="separator" />
|
||||||
|
|
||||||
|
<label class="input-label">
|
||||||
|
Password
|
||||||
|
<input class="input full-width" minlength="8" type="password" bind:value={ password } />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="button modal-secondary-action" on:click="{ createAccountInstead }">Create an account instead</button>
|
||||||
|
<button class="button button-accent modal-primary-action" on:click="{ login }" disabled="{ !buttonsEnabled }">Log In</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -3,10 +3,12 @@
|
||||||
import EditChannel from "./EditChannel.svelte";
|
import EditChannel from "./EditChannel.svelte";
|
||||||
import CreateChannel from "./CreateChannel.svelte";
|
import CreateChannel from "./CreateChannel.svelte";
|
||||||
import Toast from "./Toast.svelte";
|
import Toast from "./Toast.svelte";
|
||||||
|
import Login from "./Login.svelte";
|
||||||
|
import CreateAccount from "./CreateAccount.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $overlayStore.createChannel}
|
{#if $overlayStore.createChannel}
|
||||||
<CreateChannel { ...$overlayStore.createChannel } />
|
<CreateChannel />
|
||||||
{/if}
|
{/if}
|
||||||
{#if $overlayStore.editChannel}
|
{#if $overlayStore.editChannel}
|
||||||
<EditChannel { ...$overlayStore.editChannel } />
|
<EditChannel { ...$overlayStore.editChannel } />
|
||||||
|
@ -14,3 +16,9 @@
|
||||||
{#if $overlayStore.toast}
|
{#if $overlayStore.toast}
|
||||||
<Toast { ...$overlayStore.toast } />
|
<Toast { ...$overlayStore.toast } />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $overlayStore.login}
|
||||||
|
<Login />
|
||||||
|
{/if}
|
||||||
|
{#if $overlayStore.createAccount}
|
||||||
|
<CreateAccount />
|
||||||
|
{/if}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.toast {
|
.toast {
|
||||||
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
import logging from "./logging";
|
import logging from "./logging";
|
||||||
import { getAuthToken, getItem } from "./storage";
|
import { getAuthToken, getItem } from "./storage";
|
||||||
|
|
||||||
|
export const GatewayErrors = {
|
||||||
|
BAD_PAYLOAD: 4001,
|
||||||
|
BAD_AUTH: 4002,
|
||||||
|
AUTHENTICATION_TIMEOUT: 4003,
|
||||||
|
NO_PING: 4004,
|
||||||
|
FLOODING: 4005,
|
||||||
|
ALREADY_AUTHENTICATED: 4006,
|
||||||
|
PAYLOAD_TOO_LARGE: 4007,
|
||||||
|
TOO_MANY_SESSIONS: 4008,
|
||||||
|
};
|
||||||
|
|
||||||
export const GatewayPayloadType = {
|
export const GatewayPayloadType = {
|
||||||
Hello: 0,
|
Hello: 0,
|
||||||
Authenticate: 1,
|
Authenticate: 1,
|
||||||
|
@ -20,7 +31,8 @@ export const GatewayEventType = {
|
||||||
...GatewayPayloadType,
|
...GatewayPayloadType,
|
||||||
|
|
||||||
Open: -5,
|
Open: -5,
|
||||||
Close: -4
|
Close: -4,
|
||||||
|
BadAuth: -3,
|
||||||
}
|
}
|
||||||
|
|
||||||
const log = logging.logger("Gateway", true);
|
const log = logging.logger("Gateway", true);
|
||||||
|
@ -35,10 +47,10 @@ export default {
|
||||||
reconnectDelay: 400,
|
reconnectDelay: 400,
|
||||||
reconnectTimeout: null,
|
reconnectTimeout: null,
|
||||||
handlers: new Map(),
|
handlers: new Map(),
|
||||||
init() {
|
init(token) {
|
||||||
const token = getAuthToken();
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
log("no auth token, skipping connection");
|
log("no auth token, skipping connection");
|
||||||
|
this.dispatch(GatewayEventType.BadAuth, 0);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
log(`connecting to gateway - gatewayBase: ${getItem("gatewayBase")}`);
|
log(`connecting to gateway - gatewayBase: ${getItem("gatewayBase")}`);
|
||||||
|
@ -84,10 +96,7 @@ export default {
|
||||||
|
|
||||||
this.dispatch(payload.t, payload.d);
|
this.dispatch(payload.t, payload.d);
|
||||||
};
|
};
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = ({ code }) => {
|
||||||
if (this.reconnectDelay < 60000) {
|
|
||||||
this.reconnectDelay *= 2;
|
|
||||||
}
|
|
||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.channels = null;
|
this.channels = null;
|
||||||
|
@ -95,11 +104,21 @@ export default {
|
||||||
if (this.heartbeatInterval) {
|
if (this.heartbeatInterval) {
|
||||||
clearInterval(this.heartbeatInterval);
|
clearInterval(this.heartbeatInterval);
|
||||||
}
|
}
|
||||||
this.reconnectTimeout = setTimeout(() => {
|
|
||||||
this.init();
|
|
||||||
}, this.reconnectDelay);
|
|
||||||
|
|
||||||
this.dispatch(GatewayEventType.Close, null);
|
if (code === GatewayErrors.BAD_AUTH) {
|
||||||
|
this.dispatch(GatewayEventType.BadAuth, 1);
|
||||||
|
if (this.reconnectTimeout)
|
||||||
|
clearTimeout(this.reconnectTimeout);
|
||||||
|
} else {
|
||||||
|
if (this.reconnectDelay < 60000) {
|
||||||
|
this.reconnectDelay *= 2;
|
||||||
|
}
|
||||||
|
this.reconnectTimeout = setTimeout(() => {
|
||||||
|
this.init();
|
||||||
|
}, this.reconnectDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatch(GatewayEventType.Close, code);
|
||||||
|
|
||||||
log("close");
|
log("close");
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import App from './components/App.svelte';
|
import App from './components/App.svelte';
|
||||||
import gateway from './gateway';
|
import gateway from './gateway';
|
||||||
import { initStorageDefaults } from './storage';
|
import { getAuthToken, initStorageDefaults } from './storage';
|
||||||
import logging from "./logging";
|
import logging from "./logging";
|
||||||
|
import { authWithToken } from './auth';
|
||||||
|
|
||||||
window.__waffle = {
|
window.__waffle = {
|
||||||
logging,
|
logging,
|
||||||
|
@ -9,7 +10,7 @@ window.__waffle = {
|
||||||
};
|
};
|
||||||
|
|
||||||
initStorageDefaults();
|
initStorageDefaults();
|
||||||
gateway.init();
|
authWithToken(getAuthToken());
|
||||||
|
|
||||||
// Remove loading screen
|
// Remove loading screen
|
||||||
const loadingElement = document.getElementById("pre--loading-screen");
|
const loadingElement = document.getElementById("pre--loading-screen");
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const defaults = {
|
const defaults = {
|
||||||
apiBase: `${window.location.origin}/api/v1`,
|
apiBase: `${window.location.origin}/api/v1`,
|
||||||
gatewayBase: `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/gateway`,
|
gatewayBase: `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/gateway`
|
||||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjUwODQzMjY1LCJleHAiOjE2NTEwMTYwNjV9.ssu-MlMkwKQOcP5nmJ98KbqudcGW5XBYPc_d6et4oxo"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dummyProvider = {
|
const dummyProvider = {
|
||||||
|
|
|
@ -229,7 +229,10 @@ class OverlayStore extends Store {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
createChannel: null,
|
createChannel: null,
|
||||||
editChannel: null
|
editChannel: null,
|
||||||
|
toast: null,
|
||||||
|
login: null,
|
||||||
|
createAccount: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue