frontend: add auth overlays

This commit is contained in:
hippoz 2022-04-26 22:45:40 +03:00
parent 706372716e
commit 82926ab172
Signed by: hippoz
GPG key ID: 7C52899193467641
10 changed files with 232 additions and 17 deletions

View file

@ -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
View 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();

View 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>

View 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>

View file

@ -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}

View file

@ -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;

View file

@ -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");
}; };

View file

@ -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");

View file

@ -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 = {

View file

@ -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
}); });
} }