Authentication refactor (#65)

* Refactor login logic

* Derive username from user + fix initial redirection if logged in

* Simplify how login navigation works

* Restore redirection

* Redirect to login page when not logged in

* Fix PR issues

* Add missing refreshSuperadmin when reloading a page with a valid token

* Explicitly clearing stores when logging out.

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
Faton Ramadani
2022-05-24 17:05:40 +02:00
committed by GitHub
co-authored by Ruben Fiszel
parent 06eb50fbf2
commit 9e6ab11484
7 changed files with 191 additions and 230 deletions
+103
View File
@@ -1 +1,104 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { SvelteToast } from '@zerodevx/svelte-toast'
import { onMount } from 'svelte'
import { UserService, WorkspaceService } from '../gen'
import {
clearStores,
superadmin,
usernameStore,
usersWorkspaceStore,
workspaceStore
} from '../stores'
import { getUser, logout, logoutWithRedirect, refreshSuperadmin, sendUserToast } from '../utils'
// Default toast options
const toastOptions = {
duration: 4000, // duration of progress bar tween to the `next` value
initial: 1, // initial progress bar value
next: 0, // next progress value
pausable: false, // pause progress bar tween on mouse hover
dismissable: true, // allow dismiss with close button
reversed: false, // insert new toast to bottom of stack
intro: { x: 256 }, // toast intro fly animation settings
theme: {} // css var overrides
}
const monacoEditorUnhandledErrors = [
'Model not found',
'Connection is disposed.',
'Connection got disposed.'
]
async function redirectIfLoggedIn(): Promise<void> {
try {
await UserService.getCurrentEmail()
goto('/')
} catch {
clearStores()
}
}
async function loadData() {
try {
$usersWorkspaceStore = await WorkspaceService.listUserWorkspaces()
await refreshSuperadmin()
if ($workspaceStore && $usernameStore) {
await getUser($workspaceStore)
} else if ($superadmin) {
console.log('You are a superadmin, you can go wherever you please')
} else {
goto('/user/workspaces')
}
} catch {
logoutWithRedirect($page.url.pathname)
}
}
async function handleRedirectionsAndLoadData() {
if ($page.url.pathname === '/user/login') {
redirectIfLoggedIn()
} else {
loadData()
}
}
onMount(() => {
handleRedirectionsAndLoadData()
window.onunhandledrejection = (event: PromiseRejectionEvent) => {
event.preventDefault()
if (event.reason?.message) {
const { message, body, status } = event.reason
// Unhandled errors from Monaco Editor don't logout the user
if (monacoEditorUnhandledErrors.includes(message)) {
return
}
if (status == '401') {
sendUserToast('Logged out after a request was unauthorized', true)
logout($page.url.pathname)
} else {
sendUserToast(`${message}: ${body ?? ''}`, true)
}
} else {
console.log('Caught unhandled promise rejection without message', event)
}
}
})
</script>
<slot />
<SvelteToast {toastOptions} />
<style>
:root {
--toastBackground: #eff6ff;
--toastBarBackground: #eff6ff;
--toastColor: #123456;
}
</style>
@@ -1,38 +1,27 @@
<script lang="ts">
import '../app.css'
import { OpenAPI, UserService, WorkspaceService } from '../gen'
import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
import {
logout,
clickOutside,
sendUserToast,
logoutWithRedirect,
getUser,
refreshSuperadmin
} from '../utils'
import { onMount } from 'svelte'
import Icon from 'svelte-awesome'
import {
faScroll,
faPlay,
faWallet,
faEye,
faChevronDown,
faChevronRight,
faChevronLeft,
faBookOpen,
faCubes,
faCalendar,
faRobot,
faChevronDown,
faChevronLeft,
faChevronRight,
faCog,
faUser,
faCrown,
faCubes,
faEye,
faPlay,
faRobot,
faScroll,
faUser,
faUsersCog,
faWallet,
faWind
} from '@fortawesome/free-solid-svg-icons'
import { SvelteToast } from '@zerodevx/svelte-toast'
import { faDiscord, faGithub, faPython } from '@fortawesome/free-brands-svg-icons'
import { page } from '$app/stores'
import { onMount } from 'svelte'
import Icon from 'svelte-awesome'
import '../app.css'
import { OpenAPI } from '../gen'
import {
superadmin,
usernameStore,
@@ -40,22 +29,10 @@
usersWorkspaceStore,
workspaceStore
} from '../stores'
import { goto } from '$app/navigation'
import { clickOutside, logout } from '../utils'
OpenAPI.WITH_CREDENTIALS = true
// Default toast options
const toastOptions = {
duration: 4000, // duration of progress bar tween to the `next` value
initial: 1, // initial progress bar value
next: 0, // next progress value
pausable: false, // pause progress bar tween on mouse hover
dismissable: true, // allow dismiss with close button
reversed: false, // insert new toast to bottom of stack
intro: { x: 256 }, // toast intro fly animation settings
theme: {} // css var overrides
}
let menuOpen = false
let workspacePickerOpen = false
let isMobile = false
@@ -79,63 +56,7 @@
workspacePickerOpen = false
}
async function loadUserInfo() {
refreshSuperadmin()
if (!$usersWorkspaceStore) {
try {
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
} catch {}
}
if ($usersWorkspaceStore) {
if (!$workspaceStore) {
workspaceStore.set(localStorage.getItem('workspace')?.toString())
}
if ($workspaceStore && $usernameStore) {
await getUser($workspaceStore)
} else if ($superadmin) {
console.log('You are a superadmin, you can go wherever you please')
} else {
goto('/user/workspaces')
}
} else {
logoutWithRedirect($page.url.pathname)
}
}
$: {
if ($workspaceStore) {
localStorage.setItem('workspace', $workspaceStore)
}
}
onMount(() => {
loadUserInfo()
window.onunhandledrejection = (e) => {
if (e.reason && e.reason.message) {
if (
['Model not found', 'Connection is disposed.', 'Connection got disposed.'].includes(
e.reason.message
)
) {
// monaco editor promise cancelation
console.log('caught expected error')
} else {
if (e.reason.status == '401') {
sendUserToast('Logged out after a request was unauthorized', true)
logout($page.url.pathname)
} else {
let message = `${e.reason?.message}: ${e.reason?.body ?? ''}`
sendUserToast(message, true)
}
}
} else {
console.log('unexpected error ignored', e)
}
e.preventDefault()
return false
}
isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
//Mobile
isCollapsed = isMobile
@@ -425,7 +346,6 @@
: 'pl-44'} pr-8 flex h-full max-w-screen flex-col items-center"
>
<slot />
<SvelteToast {toastOptions} />
</div>
</div>
@@ -436,10 +356,4 @@
.menu-link {
@apply flex flex-row h-10 transform hover:translate-x-1 transition-transform ease-in duration-200 text-gray-200 hover:text-white;
}
:root {
--toastBackground: #eff6ff;
--toastBarBackground: #eff6ff;
--toastColor: #123456;
}
</style>
+14 -16
View File
@@ -1,10 +1,4 @@
<script lang="ts">
import Fuse from 'fuse.js'
import { ScriptService } from '../gen'
import type { Script } from '../gen'
import { sendUserToast, groupBy, truncateHash, canWrite } from '../utils'
import Icon from 'svelte-awesome'
import {
faArchive,
faCalendarAlt,
@@ -15,18 +9,22 @@
faPlus,
faShare
} from '@fortawesome/free-solid-svg-icons'
import Dropdown from './components/Dropdown.svelte'
import PageHeader from './components/PageHeader.svelte'
import Modal from './components/Modal.svelte'
import Tooltip from './components/Tooltip.svelte'
import ShareModal from './components/ShareModal.svelte'
import SharedBadge from './components/SharedBadge.svelte'
import { superadmin, usernameStore, userStore, workspaceStore } from '../stores'
import CenteredPage from './components/CenteredPage.svelte'
import Tabs from './components/Tabs.svelte'
import Fuse from 'fuse.js'
import Icon from 'svelte-awesome'
import { slide } from 'svelte/transition'
import type { Script } from '../gen'
import { ScriptService } from '../gen'
import { superadmin, usernameStore, userStore, workspaceStore } from '../stores'
import { canWrite, groupBy, sendUserToast, truncateHash } from '../utils'
import Badge from './components/Badge.svelte'
import CenteredPage from './components/CenteredPage.svelte'
import Dropdown from './components/Dropdown.svelte'
import Modal from './components/Modal.svelte'
import PageHeader from './components/PageHeader.svelte'
import SharedBadge from './components/SharedBadge.svelte'
import ShareModal from './components/ShareModal.svelte'
import Tabs from './components/Tabs.svelte'
import Tooltip from './components/Tooltip.svelte'
type Tab = 'all' | 'personal' | 'groups' | 'shared' | 'community'
type Section = [string, ScriptW[]]
@@ -1,68 +1,3 @@
<script lang="ts">
import { SvelteToast } from '@zerodevx/svelte-toast'
import { logout, refreshSuperadmin, sendUserToast } from '../../utils'
import { onMount } from 'svelte'
import { page } from '$app/stores'
import { superadmin, userStore, workspaceStore } from '../../stores'
// Default options
const toastOptions = {
duration: 4000, // duration of progress bar tween to the `next` value
initial: 1, // initial progress bar value
next: 0, // next progress value
pausable: false, // pause progress bar tween on mouse hover
dismissable: true, // allow dismiss with close button
reversed: false, // insert new toast to bottom of stack
intro: { x: 256 }, // toast intro fly animation settings
theme: {} // css var overrides
}
$: {
if ($workspaceStore) {
localStorage.setItem('workspace', $workspaceStore)
}
}
onMount(() => {
window.onunhandledrejection = (e) => {
e.preventDefault()
if (e.reason && e.reason.message) {
if (
['Model not found', 'Connection is disposed.', 'Connection got disposed.'].includes(
e.reason.message
)
) {
// monaco editor promise cancelation
console.log('caught expected error')
}
if (e.reason.status == '401') {
sendUserToast('Logged out after a request was unauthorized', true)
logout($page.url.pathname)
} else {
let message = `${e.reason?.message}: ${e.reason?.body ?? ''}`
sendUserToast(message, true)
}
} else {
console.log('unexpected error ignored', e)
}
e.preventDefault()
return false
}
workspaceStore.set(undefined)
userStore.set(undefined)
refreshSuperadmin()
})
</script>
<div class="min-h-screen antialiased text-gray-900">
<slot />
<SvelteToast {toastOptions} />
</div>
<style>
:root {
--toastBackground: #eff6ff;
--toastBarBackground: #eff6ff;
--toastColor: #123456;
}
</style>
+22 -27
View File
@@ -1,15 +1,13 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { UserService } from '../../gen'
import { refreshSuperadmin, sendUserToast } from '../../utils'
import { page } from '$app/stores'
import { userStore, usersWorkspaceStore, workspaceStore } from '../../stores'
import CenteredModal from './CenteredModal.svelte'
import Icon from 'svelte-awesome'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import Icon from 'svelte-awesome'
import { slide } from 'svelte/transition'
import { onMount } from 'svelte'
import { UserService, WorkspaceService } from '../../gen'
import { userStore, usersWorkspaceStore, workspaceStore } from '../../stores'
import { getUser, refreshSuperadmin, sendUserToast } from '../../utils'
import CenteredModal from './CenteredModal.svelte'
let email = $page.url.searchParams.get('email') ?? ''
let password = $page.url.searchParams.get('password') ?? ''
@@ -20,13 +18,20 @@
async function login(): Promise<void> {
try {
await UserService.login({
requestBody: {
email: email,
password
}
})
const requestBody = {
email,
password
}
await UserService.login({ requestBody })
// Once logged in, we can fetch the workspaces
$usersWorkspaceStore = await WorkspaceService.listUserWorkspaces()
// And the actual user
$userStore = await getUser($workspaceStore!)
// Finally, we check whether the user is a superadmin
refreshSuperadmin()
if (rd) {
goto(decodeURI(rd))
} else {
@@ -39,30 +44,20 @@
function handleKeyUp(event: KeyboardEvent) {
const key = event.key || event.keyCode
if (key === 13 || key === 'Enter') {
event.preventDefault()
login()
}
}
async function redirectIfLoggedIn() {
try {
await UserService.getCurrentEmail()
goto('/')
} catch {
usersWorkspaceStore.set(undefined)
workspaceStore.set(undefined)
userStore.set(undefined)
}
if (error) {
sendUserToast(error, true)
}
onMount(() => {
setTimeout(redirectIfLoggedIn, 1000)
})
</script>
<!-- Enable submit form on enter -->
<CenteredModal subtitle={error}>
<CenteredModal>
<div class="justify-center text-center flex flex-col">
<span class="text-xs text-gray-600">Currently only signup through Github is supported</span>
<a rel="external" href="/api/oauth/login/github"
+21 -2
View File
@@ -1,5 +1,6 @@
import { writable, derived, readable } from 'svelte/store'
import { browser } from '$app/env'
import type { Readable } from 'svelte/store'
import { derived, writable } from 'svelte/store'
import type { UserWorkspaceList } from './gen'
export interface UserExt {
@@ -10,9 +11,12 @@ export interface UserExt {
groups: string[]
pgroups: string[]
}
let persistedWorkspace = browser && localStorage.getItem('workspace')
export const userStore = writable<UserExt | undefined>(undefined)
export const workspaceStore = writable<string | undefined>(undefined)
export let workspaceStore = writable<string | undefined>(
persistedWorkspace ? String(persistedWorkspace) : undefined
)
export const usersWorkspaceStore = writable<UserWorkspaceList | undefined>(undefined)
export const usernameStore: Readable<string | undefined> = derived(
[usersWorkspaceStore, workspaceStore],
@@ -21,3 +25,18 @@ export const usernameStore: Readable<string | undefined> = derived(
}
)
export const superadmin = writable<String | false | undefined>(undefined)
if (browser) {
workspaceStore.subscribe((workspace) => {
if (workspace) {
localStorage.setItem('workspace', String(workspace))
}
})
}
export function clearStores(): void {
userStore.set(undefined)
workspaceStore.set(undefined)
usersWorkspaceStore.set(undefined)
superadmin.set(undefined)
}
+15 -18
View File
@@ -1,10 +1,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { toast } from '@zerodevx/svelte-toast'
import { CancelablePromise, UserService } from './gen'
import { superadmin, userStore, workspaceStore } from './stores'
import type { UserExt } from './stores'
import { get } from 'svelte/store'
import { goto } from '$app/navigation'
import { toast } from '@zerodevx/svelte-toast'
import { get } from 'svelte/store'
import { CancelablePromise, UserService, type User } from './gen'
import { clearStores, superadmin, userStore, workspaceStore, type UserExt } from './stores'
export function isToday(someDate: Date): boolean {
const today = new Date()
@@ -76,14 +75,7 @@ export function truncateHash(hash: string): string {
async function loadStore(workspace: string): Promise<UserExt | undefined> {
try {
const user = await UserService.whoami({ workspace })
const nuser = {
username: user.username,
email: user.email,
created_at: user.created_at,
is_admin: user.is_admin,
groups: user.groups!,
pgroups: user.groups!.map((x) => `g/${x}`)
}
const nuser = mapUserToUserExt(user)
userStore.set(nuser)
return nuser
} catch (error) {
@@ -151,7 +143,7 @@ export async function refreshSuperadmin(): Promise<void> {
export async function logout(logoutMessage?: string): Promise<void> {
try {
superadmin.set(undefined)
clearStores()
goto(`/user/login${logoutMessage ? '?error=' + encodeURIComponent(logoutMessage) : ''}`)
await UserService.logout()
sendUserToast('you have been logged out')
@@ -265,10 +257,7 @@ export function removeKeysWithEmptyValues(obj: any): any {
}
export function allTrue(dict: { [id: string]: boolean }): boolean {
for (let v of Object.values(dict)) {
if (!v) return false
}
return true
return Object.values(dict).every(Boolean)
}
export function forLater(scheduledString: string): boolean {
@@ -331,3 +320,11 @@ export function truncateRev(s: string, n: number, prefix: string = '...'): strin
export function isString(value: any) {
return typeof value === 'string' || value instanceof String
}
export function mapUserToUserExt(user: User): UserExt {
return {
...user,
groups: user.groups!,
pgroups: user.groups!.map((x) => `g/${x}`)
}
}