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%);
|
||||
}
|
||||
|
||||
.modal-backdrop-opaque {
|
||||
background-color: var(--background-color-1);
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: flex;
|
||||
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 CreateChannel from "./CreateChannel.svelte";
|
||||
import Toast from "./Toast.svelte";
|
||||
import Login from "./Login.svelte";
|
||||
import CreateAccount from "./CreateAccount.svelte";
|
||||
</script>
|
||||
|
||||
{#if $overlayStore.createChannel}
|
||||
<CreateChannel { ...$overlayStore.createChannel } />
|
||||
<CreateChannel />
|
||||
{/if}
|
||||
{#if $overlayStore.editChannel}
|
||||
<EditChannel { ...$overlayStore.editChannel } />
|
||||
|
@ -14,3 +16,9 @@
|
|||
{#if $overlayStore.toast}
|
||||
<Toast { ...$overlayStore.toast } />
|
||||
{/if}
|
||||
{#if $overlayStore.login}
|
||||
<Login />
|
||||
{/if}
|
||||
{#if $overlayStore.createAccount}
|
||||
<CreateAccount />
|
||||
{/if}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
<style>
|
||||
.toast {
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
import logging from "./logging";
|
||||
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 = {
|
||||
Hello: 0,
|
||||
Authenticate: 1,
|
||||
|
@ -20,7 +31,8 @@ export const GatewayEventType = {
|
|||
...GatewayPayloadType,
|
||||
|
||||
Open: -5,
|
||||
Close: -4
|
||||
Close: -4,
|
||||
BadAuth: -3,
|
||||
}
|
||||
|
||||
const log = logging.logger("Gateway", true);
|
||||
|
@ -35,10 +47,10 @@ export default {
|
|||
reconnectDelay: 400,
|
||||
reconnectTimeout: null,
|
||||
handlers: new Map(),
|
||||
init() {
|
||||
const token = getAuthToken();
|
||||
init(token) {
|
||||
if (!token) {
|
||||
log("no auth token, skipping connection");
|
||||
this.dispatch(GatewayEventType.BadAuth, 0);
|
||||
return false;
|
||||
}
|
||||
log(`connecting to gateway - gatewayBase: ${getItem("gatewayBase")}`);
|
||||
|
@ -84,10 +96,7 @@ export default {
|
|||
|
||||
this.dispatch(payload.t, payload.d);
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
if (this.reconnectDelay < 60000) {
|
||||
this.reconnectDelay *= 2;
|
||||
}
|
||||
this.ws.onclose = ({ code }) => {
|
||||
this.authenticated = false;
|
||||
this.user = null;
|
||||
this.channels = null;
|
||||
|
@ -95,11 +104,21 @@ export default {
|
|||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
}
|
||||
|
||||
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, null);
|
||||
this.dispatch(GatewayEventType.Close, code);
|
||||
|
||||
log("close");
|
||||
};
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import App from './components/App.svelte';
|
||||
import gateway from './gateway';
|
||||
import { initStorageDefaults } from './storage';
|
||||
import { getAuthToken, initStorageDefaults } from './storage';
|
||||
import logging from "./logging";
|
||||
import { authWithToken } from './auth';
|
||||
|
||||
window.__waffle = {
|
||||
logging,
|
||||
|
@ -9,7 +10,7 @@ window.__waffle = {
|
|||
};
|
||||
|
||||
initStorageDefaults();
|
||||
gateway.init();
|
||||
authWithToken(getAuthToken());
|
||||
|
||||
// Remove loading screen
|
||||
const loadingElement = document.getElementById("pre--loading-screen");
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const defaults = {
|
||||
apiBase: `${window.location.origin}/api/v1`,
|
||||
gatewayBase: `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/gateway`,
|
||||
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidHlwZSI6MSwiaWF0IjoxNjUwODQzMjY1LCJleHAiOjE2NTEwMTYwNjV9.ssu-MlMkwKQOcP5nmJ98KbqudcGW5XBYPc_d6et4oxo"
|
||||
gatewayBase: `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/gateway`
|
||||
};
|
||||
|
||||
const dummyProvider = {
|
||||
|
|
|
@ -229,7 +229,10 @@ class OverlayStore extends Store {
|
|||
constructor() {
|
||||
super({
|
||||
createChannel: null,
|
||||
editChannel: null
|
||||
editChannel: null,
|
||||
toast: null,
|
||||
login: null,
|
||||
createAccount: null
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue