diff --git a/package.json b/package.json index 424a4a5..6a4a607 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swaf", - "version": "0.24.10", + "version": "0.25.0", "description": "Structure Web Application Framework.", "repository": "https://eternae.ink/ashpie/swaf", "author": "Alice Gaudon ", diff --git a/src/SessionWebSocketListener.ts b/src/SessionWebSocketListener.ts new file mode 100644 index 0000000..a4241a3 --- /dev/null +++ b/src/SessionWebSocketListener.ts @@ -0,0 +1,51 @@ +import config from "config"; +import cookie from "cookie"; +import cookieParser from "cookie-parser"; +import {Request} from "express"; +import {Session} from "express-session"; +import {IncomingMessage} from "http"; +import {WebSocket} from "ws"; + +import Application from "./Application.js"; +import RedisComponent from "./components/RedisComponent.js"; +import {logger} from "./Logger.js"; +import WebSocketListener from "./WebSocketListener.js"; + +export default abstract class SessionWebSocketListener extends WebSocketListener { + + public async handle(socket: WebSocket, request: IncomingMessage): Promise { + socket.once('message', (data, isBinary) => { + if (isBinary) return socket.close(1003); + + const cookies = cookie.parse(data.toString()); + const sid = cookieParser.signedCookie(cookies['connect.sid'], config.get('session.secret')); + + if (!sid) { + socket.close(1002, 'Could not decrypt provided session cookie.'); + return; + } + + const store = this.getApp().as(RedisComponent).getStore(); + store.get(sid, (err, session) => { + if (err || !session) { + logger.error(err, 'Error while initializing session in websocket for sid ' + sid); + socket.close(1011); + return; + } + + session.id = sid; + + store.createSession(request, session); + this.handleSessionSocket(socket, request, session as Session).catch(err => { + logger.error(err, 'Error in websocket listener.'); + }); + }); + }); + } + + protected abstract handleSessionSocket( + socket: WebSocket, + request: IncomingMessage, + session: Session, + ): Promise; +} diff --git a/src/WebSocketListener.ts b/src/WebSocketListener.ts index d934774..62316b9 100644 --- a/src/WebSocketListener.ts +++ b/src/WebSocketListener.ts @@ -1,4 +1,3 @@ -import {Session} from "express-session"; import {IncomingMessage} from "http"; import WebSocket from "ws"; @@ -20,6 +19,5 @@ export default abstract class WebSocketListener { public abstract handle( socket: WebSocket, request: IncomingMessage, - session: Session | null, ): Promise; } diff --git a/src/assets/scss/base.scss b/src/assets/scss/base.scss index 22ae0fc..115aa21 100644 --- a/src/assets/scss/base.scss +++ b/src/assets/scss/base.scss @@ -155,7 +155,7 @@ h1 { a { text-decoration: none; - .icon.lucide-external-link { //todo add js + .icon.lucide-external-link { --icon-size: 16px; margin-left: 4px; margin-top: -3px; @@ -166,6 +166,13 @@ ul { list-style-type: '- '; } +hr { + margin: 0; + border: 0; + border-top: 1px solid var(--on-background); + opacity: 0.2; +} + .primary, .bold { --color: var(--primary-on-background); --background-color: var(--background); @@ -249,7 +256,7 @@ button, .button { margin-left: 8px; } - &:hover::after { + &:hover::after:not([disabled]) { content: ""; position: absolute; @@ -260,4 +267,10 @@ button, .button { background-color: var(--on-background); opacity: 0.2; } + + &[disabled] { + position: relative; + cursor: not-allowed; + opacity: 0.1; + } } diff --git a/src/assets/scss/data-table.scss b/src/assets/scss/data-table.scss index d5ce8f0..6157cc6 100644 --- a/src/assets/scss/data-table.scss +++ b/src/assets/scss/data-table.scss @@ -23,6 +23,46 @@ thead tr:hover { background-color: transparent; } + + tbody td.actions { + display: flex; + flex-direction: row; + + button { + padding: 8px; + + .icon { + margin: 0 !important; + } + + .tip { + display: none; + } + } + } + + thead th.col-grow { + width: 100%; + } + + tbody td.col-grow-cell { + > * { + display: flex; + flex-direction: row; + + > * { + width: 0; + flex-grow: 1; + white-space: nowrap; + text-overflow: ellipsis; + } + + * { + overflow: hidden; + text-overflow: ellipsis; + } + } + } } .data-table-container { diff --git a/src/assets/ts/WebsocketClient.ts b/src/assets/ts/WebsocketClient.ts index 5017d64..db01210 100644 --- a/src/assets/ts/WebsocketClient.ts +++ b/src/assets/ts/WebsocketClient.ts @@ -17,6 +17,7 @@ export default class WebsocketClient { const websocket = new WebSocket(this.websocketUrl); websocket.onopen = () => { console.debug('Websocket connected'); + websocket.send(document.cookie); }; websocket.onmessage = (e) => { this.listener(websocket, e); diff --git a/src/assets/ts/icons.ts b/src/assets/ts/icons.ts index b2bfe86..d659309 100644 --- a/src/assets/ts/icons.ts +++ b/src/assets/ts/icons.ts @@ -1,5 +1,7 @@ import {createIcons, icons} from "lucide"; +import {toLucideIconsPascalCase} from "../../common/StringUtils.js"; + let hasAlreadyReplacedIcons = false; export function replaceIcons(once: boolean): void { @@ -9,3 +11,7 @@ export function replaceIcons(once: boolean): void { hasAlreadyReplacedIcons = true; } } + +export function isLucideIcon(iconName: string): boolean { + return Object.keys(icons).indexOf(toLucideIconsPascalCase(iconName)) >= 0; +} diff --git a/src/assets/views/auth/account/account.svelte b/src/assets/views/auth/account/account.svelte index dcb0329..c7d8eaa 100644 --- a/src/assets/views/auth/account/account.svelte +++ b/src/assets/views/auth/account/account.svelte @@ -15,81 +15,87 @@ -
-
-

Personal information

+
+

+ + Personal information +

- {#if $locals.display_email_warning && $locals.emails.length <= 0} - - {/if} + {#if $locals.display_email_warning && $locals.emails.length <= 0} + + {/if} - {#each personalInfoFields as field} -

{field.name}: {field.value}

- {/each} + {#each personalInfoFields as field} +

{field.name}: {field.value}

+ {/each} - {#if mainEmail} -

Contact email: {mainEmail} More...

- {/if} + {#if mainEmail} +

Contact email: {mainEmail} More...

+ {/if} +
+ + {#if $locals.has_name_component} + + {/if} + + {#if $locals.has_password_component} + + {/if} + +
+

+ + Email addresses +

+ +
+ + + + + + + + + + + {#each emails as email} + {#if email.id === $locals.user.main_email_id} + + + + + + {/if} + {/each} + + {#each emails as email} + {#if email.id !== $locals.user.main_email_id} + + + + + + {/if} + {/each} + +
TypeAddressActions
Main{email.email}
Secondary{email.email} +
+ + + +
+ + +
- {#if $locals.has_name_component} - - {/if} - - {#if $locals.has_password_component} - - {/if} - -
-

Email addresses

- -
- - - - - - - - - - - {#each emails as email} - {#if email.id === $locals.user.main_email_id} - - - - - - {/if} - {/each} - - {#each emails as email} - {#if email.id !== $locals.user.main_email_id} - - - - - - {/if} - {/each} - -
TypeAddressActions
Main{email.email}
Secondary{email.email} -
- - - -
- - -
-
- - {#if hasRoute('add-email')} + {#if hasRoute('add-email')}

Add an email address:

@@ -98,7 +104,6 @@ hint="An email address we can use to identify you in case you lose access to your account" required/>
- {/if} -
-
+ {/if} + diff --git a/src/assets/views/auth/auth.svelte b/src/assets/views/auth/auth.svelte index 331acc4..1c6b039 100644 --- a/src/assets/views/auth/auth.svelte +++ b/src/assets/views/auth/auth.svelte @@ -6,8 +6,8 @@ import Icon from "../utils/Icon.svelte"; import {hasRoute, route} from "../../../common/Routing"; - let registerUsingMagicLink = $locals.previousFormData?.['auth_method'] !== 'password'; - let loginUsingMagicLink = true; + let registerUsingMagicLink = $locals.flash.previousFormData?.[0]?.['auth_method'] !== 'password'; + let loginUsingMagicLink = !$locals.flash.previousFormData?.[0]?.['password']; let queryStr = ''; let previousUrl: string = $locals.previousUrl as string | undefined; @@ -22,49 +22,72 @@ description="Join {$locals.app.name} and share your files!" h1="Authentication and registration"> -
- {#if hasRoute('login')} + {#if hasRoute('login')}
-

Log in

+

+ + Log in +

- {#if !loginUsingMagicLink} - - - {:else} - + {#if $locals.hasPassword} + {#if loginUsingMagicLink} + + {:else} + + + {/if} {/if} - +
- {/if} + {/if} - {#if hasRoute('register')} + {#if hasRoute('register')}
-

Register

+

+ + Register +

- {#if $locals.has_username} + {#if $locals.hasUsername} {/if} - {#if registerUsingMagicLink} + {#if registerUsingMagicLink || !$locals.canRegisterWithPassword} - + {#if $locals.canRegisterWithPassword} + + {/if} {:else} - - + + {/if} @@ -72,6 +95,5 @@
- {/if} -
+ {/if} diff --git a/src/assets/views/backend/index.svelte b/src/assets/views/backend/index.svelte index 6ad2f95..3e6f476 100644 --- a/src/assets/views/backend/index.svelte +++ b/src/assets/views/backend/index.svelte @@ -8,26 +8,24 @@ -
- + -

App administration

+

App administration

-
- -
+ diff --git a/src/assets/views/components/CopyableText.svelte b/src/assets/views/components/CopyableText.svelte index 9b6ee3b..b225196 100644 --- a/src/assets/views/components/CopyableText.svelte +++ b/src/assets/views/components/CopyableText.svelte @@ -2,8 +2,9 @@ import Icon from "../utils/Icon.svelte"; import { fade } from "svelte/transition"; - export let title: string; + export let title: string | undefined = undefined; export let content: string; + export let buttonMode: boolean = false; let contentNode: HTMLElement; let copiedOverlay: HTMLElement; @@ -68,28 +69,55 @@ margin: 8px; } } - - .copied-overlay { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - padding: 8px; - - text-align: center; - background-color: var(--success); - } } + + .button-mode-button { + position: relative; + } + + .copied-overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + padding: 8px; + + text-align: center; + background-color: var(--success); + } + + .content.hidden { + pointer-events: none; + overflow: hidden; + width: 0; + height: 0; + margin: 0; + padding: 0; + position: absolute; + } -
-
{title}
-
{content}
- +{#if buttonMode} + +
+ {#if showCopiedOverlay} +
+ {/if} + +{:else} +
+ {#if title} +
{title}
+ {/if} +
{content}
+ + + {#if showCopiedOverlay} +
Copied!
+ {/if} +
+{/if} diff --git a/src/assets/views/components/FlashMessages.svelte b/src/assets/views/components/FlashMessages.svelte index c49d8e4..4996325 100644 --- a/src/assets/views/components/FlashMessages.svelte +++ b/src/assets/views/components/FlashMessages.svelte @@ -3,6 +3,13 @@ import Message from "./Message.svelte"; export let flashed = $locals.flash; + const displayedCategories = [ + 'success', + 'info', + 'warning', + 'error', + 'error-alert', + ]; + +
+ +
+
    + +
diff --git a/src/assets/views/components/NavMenuItem.svelte b/src/assets/views/components/NavMenuItem.svelte index 6e40479..546202b 100644 --- a/src/assets/views/components/NavMenuItem.svelte +++ b/src/assets/views/components/NavMenuItem.svelte @@ -6,28 +6,35 @@ export let icon; export let text; export let action = false; + export let hovered = false; + + function onMouseEnter() { + hovered = true; + } + + function onMouseLeave() { + hovered = false; + } -
  • +
  • {#if action}
    {:else} @@ -77,4 +90,5 @@ {text} {/if} +
  • diff --git a/src/assets/views/home.svelte b/src/assets/views/home.svelte index 4dc7521..50affd5 100644 --- a/src/assets/views/home.svelte +++ b/src/assets/views/home.svelte @@ -18,16 +18,16 @@

    Welcome to {$locals.app.name}!

    {#if hasAnyRoute('tests', 'design')} - + {/if}
    diff --git a/src/assets/views/magic_link.svelte b/src/assets/views/magic_link.svelte index fc3346b..4b2f0f7 100644 --- a/src/assets/views/magic_link.svelte +++ b/src/assets/views/magic_link.svelte @@ -8,14 +8,12 @@ -
    -
    - {#if $locals.err} - - {:else} - -

    You can now close this page.

    - {/if} -
    +
    + {#if $locals.err} + + {:else} + +

    You can now close this page.

    + {/if}
    diff --git a/src/assets/views/magic_link_lobby.svelte b/src/assets/views/magic_link_lobby.svelte index 2999794..e60d86a 100644 --- a/src/assets/views/magic_link_lobby.svelte +++ b/src/assets/views/magic_link_lobby.svelte @@ -38,14 +38,12 @@ -
    -
    - - +
    + + -

    Waiting for you to open the link...

    -
    +

    Waiting for you to open the link...

    diff --git a/src/assets/views/templates/base/BaseNavMenuAuth.svelte b/src/assets/views/templates/base/BaseNavMenuAuth.svelte index 03b4544..0a384be 100644 --- a/src/assets/views/templates/base/BaseNavMenuAuth.svelte +++ b/src/assets/views/templates/base/BaseNavMenuAuth.svelte @@ -2,8 +2,11 @@ import {locals} from "../../../ts/stores.js"; import NavMenuItem from "../../components/NavMenuItem.svelte"; import {hasRoute, route} from "../../../../common/Routing"; + import NavMenuDropdown from "../../components/NavMenuDropdown.svelte"; + import BaseNavMenuAuthAccountDropdownAdditionalLinks from "./BaseNavMenuAuthAccountDropdownAdditionalLinks.svelte"; export let noLoginLink = false; + let accountItemHovered; {#if hasRoute('auth')} @@ -12,8 +15,12 @@ {/if} - - + + + + + + {:else if !noLoginLink} {/if} diff --git a/src/assets/views/templates/base/BaseNavMenuAuthAccountDropdownAdditionalLinks.svelte b/src/assets/views/templates/base/BaseNavMenuAuthAccountDropdownAdditionalLinks.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/assets/views/utils/Field.svelte b/src/assets/views/utils/Field.svelte index 18292e7..8c0e38f 100644 --- a/src/assets/views/utils/Field.svelte +++ b/src/assets/views/utils/Field.svelte @@ -8,19 +8,19 @@ export let type: string; export let name: string; - type FieldValue = string | number | Record; + type FieldValue = string | number | boolean | Record; export let value: FieldValue | undefined = undefined; export let initialValue: FieldValue | undefined = undefined; export let placeholder: string | undefined = undefined; export let hint: string | undefined = undefined; export let extraData: string[] | undefined = undefined; export let icon: string | undefined = undefined; - export let validation = $locals.validation?.[name] as { message: string, value?: string } | undefined; + export let validation = $locals.flash.validation?.[0]?.[name] as { message: string, value?: string } | undefined; const formId = getContext('formId'); const fieldId = `${formId}-${name}-field`; - const previousFormData = $locals.previousFormData as FieldValue | undefined || []; + const previousFormData = $locals.flash.previousFormData?.[0] as Record | undefined || {}; let previousFieldData = previousFormData[name]; if (typeof value === 'number' && previousFieldData) previousFieldData = Number(previousFieldData); @@ -45,9 +45,13 @@ } } - function focusInput() { + function focusInput(e) { if (input) { - if (['file', 'checkbox', 'color'].indexOf(input.type) >= 0) { + if (input.type === 'checkbox') { + if (e.target !== label && e.target !== input) { + input.click(); + } + } else if (['file', 'color'].indexOf(input.type) >= 0) { input.click(); } else { input.focus(); @@ -58,31 +62,37 @@ } function handleInput() { - // in here, you can switch on type and implement - // whatever behaviour you need - value = type.match(/^(number|range)$/) - ? +this.value - : this.value; - - if (this.type === 'file') { - handleFileInput(); - } - - if (this.type === 'datetime-local') { - value = dateToIsoString(new Date(this.value)); + switch (this.type) { + case 'number': + case 'range': + value = +this.value; + break; + case 'file': + handleFileInput(); + break; + case 'datetime-local': + value = dateToIsoString(new Date(this.value)); + break; + case 'checkbox': + value = !!this.checked; + break; + default: + value = this.value; + break; } } let input: HTMLInputElement; + let label: HTMLLabelElement; function chooseFile() { input.click(); } - let files: FileList | undefined; + export let fileList: FileList | undefined = undefined; function handleFileInput() { - files = input.files; + fileList = input.files; } let focused = false; @@ -98,6 +108,7 @@ .control { position: relative; + z-index: 0; display: flex; align-items: start; flex-direction: row; @@ -423,7 +434,8 @@ on:focusout={() => focused = false}>
    {:else if type === 'checkbox'} - + {:else if type === 'datetime-local'} @@ -434,15 +446,15 @@
    {#if type !== 'duration'} -