diff --git a/finnow-api/source/api_mapping.d b/finnow-api/source/api_mapping.d index c09a99c..16be4d5 100644 --- a/finnow-api/source/api_mapping.d +++ b/finnow-api/source/api_mapping.d @@ -40,6 +40,7 @@ HttpRequestHandler mapApiHandlers() { a.map(HttpMethod.POST, "/profiles", &handleCreateNewProfile); /// URL path to a specific profile, with the :profile path parameter. const PROFILE_PATH = "/profiles/:profile"; + a.map(HttpMethod.GET, PROFILE_PATH, &handleGetProfile); a.map(HttpMethod.DELETE, PROFILE_PATH, &handleDeleteProfile); a.map(HttpMethod.GET, PROFILE_PATH ~ "/properties", &handleGetProperties); diff --git a/finnow-api/source/profile/api.d b/finnow-api/source/profile/api.d index e6089dd..752e2b2 100644 --- a/finnow-api/source/profile/api.d +++ b/finnow-api/source/profile/api.d @@ -38,6 +38,11 @@ void handleGetProfiles(ref ServerHttpRequest request, ref ServerHttpResponse res writeJsonBody(response, profiles); } +void handleGetProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) { + ProfileContext profileCtx = getProfileContextOrThrow(request); + writeJsonBody(response, profileCtx.profile); +} + void handleDeleteProfile(ref ServerHttpRequest request, ref ServerHttpResponse response) { string name = request.getPathParamAs!string("profile"); if (!validateProfileName(name)) { diff --git a/web-app/package-lock.json b/web-app/package-lock.json index 215f3b1..3b796e3 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -8,6 +8,10 @@ "name": "web-app", "version": "0.0.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.0.0", + "@fortawesome/free-regular-svg-icons": "^7.0.0", + "@fortawesome/free-solid-svg-icons": "^7.0.0", + "@fortawesome/vue-fontawesome": "^3.1.1", "pinia": "^3.0.3", "vue": "^3.5.18", "vue-router": "^4.5.1" @@ -1150,6 +1154,61 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.0.tgz", + "integrity": "sha512-PGMrIYXLGA5K8RWy8zwBkd4vFi4z7ubxtet6Yn13Plf6krRTwPbdlCwlcfmoX0R7B4Z643QvrtHmdQ5fNtfFCg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.0.0.tgz", + "integrity": "sha512-obBEF+zd98r/KtKVW6A+8UGWeaOoyMpl6Q9P3FzHsOnsg742aXsl8v+H/zp09qSSu/a/Hxe9LNKzbBaQq1CEbA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.0.0.tgz", + "integrity": "sha512-qAh0mTaCY22sQzMK2lKBrtn/aR4keUu5XmtdYR7d702laMe0h+Ab4Kj2pExR9HZkKhjKoq8pbwt8Td+mjW/ipQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.0.0.tgz", + "integrity": "sha512-njSLAllkOddYDCXgTFboXn54Oe5FcvpkWq+FoetOHR64PbN0608kM02Lze0xtISGpXgP+i26VyXRQA0Irh3Obw==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/vue-fontawesome": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.1.1.tgz", + "integrity": "sha512-U5azn4mcUVpjHe4JO0Wbe7Ih8e3VbN83EH7OTBtA5/QGw9qcPGffqcmwsLyZYgEkpVkYbq/6dX1Iyl5KUGMp6Q==", + "license": "MIT", + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7", + "vue": ">= 3.0.0 < 4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", diff --git a/web-app/package.json b/web-app/package.json index 02901a0..ec5fc5c 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -16,6 +16,10 @@ "format": "prettier --write src/" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^7.0.0", + "@fortawesome/free-regular-svg-icons": "^7.0.0", + "@fortawesome/free-solid-svg-icons": "^7.0.0", + "@fortawesome/vue-fontawesome": "^3.1.1", "pinia": "^3.0.3", "vue": "^3.5.18", "vue-router": "^4.5.1" diff --git a/web-app/src/api/profile.ts b/web-app/src/api/profile.ts index 2a057e9..a47a007 100644 --- a/web-app/src/api/profile.ts +++ b/web-app/src/api/profile.ts @@ -14,6 +14,10 @@ export class ProfileApiClient extends ApiClient { return await super.getJson('/profiles') } + async getProfile(name: string): Promise { + return await super.getJson('/profiles/' + name) + } + async createProfile(name: string): Promise { return await super.postJson('/profiles', { name }) } diff --git a/web-app/src/api/token-util.ts b/web-app/src/api/token-util.ts new file mode 100644 index 0000000..548b0fa --- /dev/null +++ b/web-app/src/api/token-util.ts @@ -0,0 +1,23 @@ +export function parseExpiration(token: string): number { + const parts = token.split('.') + if (parts.length !== 3) return 0 + const payload = JSON.parse(atob(parts[1])) + return payload.exp +} + +export function secondsUntilExpired(token: string): number { + const exp = parseExpiration(token) + const now = Date.now() / 1000 + return exp - now +} + +export function isExpired(token: string): boolean { + return secondsUntilExpired(token) <= 0 +} + +export function parseSubject(token: string): string { + const parts = token.split('.') + if (parts.length !== 3) return '' + const payload = JSON.parse(atob(parts[1])) + return payload.sub +} diff --git a/web-app/src/assets/fonts/open-sans/OFL.txt b/web-app/src/assets/fonts/open-sans/OFL.txt new file mode 100644 index 0000000..cb7002a --- /dev/null +++ b/web-app/src/assets/fonts/open-sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/web-app/src/assets/fonts/open-sans/OpenSans-Italic-VariableFont_wdth,wght.ttf b/web-app/src/assets/fonts/open-sans/OpenSans-Italic-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..8312b2c Binary files /dev/null and b/web-app/src/assets/fonts/open-sans/OpenSans-Italic-VariableFont_wdth,wght.ttf differ diff --git a/web-app/src/assets/fonts/open-sans/OpenSans-VariableFont_wdth,wght.ttf b/web-app/src/assets/fonts/open-sans/OpenSans-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..ac587b4 Binary files /dev/null and b/web-app/src/assets/fonts/open-sans/OpenSans-VariableFont_wdth,wght.ttf differ diff --git a/web-app/src/assets/fonts/playwrite-nl/OFL.txt b/web-app/src/assets/fonts/playwrite-nl/OFL.txt new file mode 100644 index 0000000..1efea05 --- /dev/null +++ b/web-app/src/assets/fonts/playwrite-nl/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2023 The Playwrite Project Authors (https://github.com/TypeTogether/Playwrite) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/web-app/src/assets/fonts/playwrite-nl/PlaywriteNL-VariableFont_wght.ttf b/web-app/src/assets/fonts/playwrite-nl/PlaywriteNL-VariableFont_wght.ttf new file mode 100644 index 0000000..a115395 Binary files /dev/null and b/web-app/src/assets/fonts/playwrite-nl/PlaywriteNL-VariableFont_wght.ttf differ diff --git a/web-app/src/assets/main.css b/web-app/src/assets/main.css new file mode 100644 index 0000000..5c47a10 --- /dev/null +++ b/web-app/src/assets/main.css @@ -0,0 +1,61 @@ +:root { + --theme-primary: #01bbff; + --theme-secondary: #00759f; + --theme-tertiary: #01ffc4; + --bg-primary: #00131a; + --bg-secondary: #003447; + --bg-page: #000b0f; +} + +@font-face { + font-family: 'OpenSans'; + src: url('fonts/open-sans/OpenSans-VariableFont_wdth,wght.ttf'); + font-style: normal; +} + +@font-face { + font-family: 'OpenSans'; + src: url('fonts/open-sans/OpenSans-Italic-VariableFont_wdth,wght.ttf'); + font-style: italic; +} + +@font-face { + font-family: 'PlaywriteNL'; + src: url('fonts/playwrite-nl/PlaywriteNL-VariableFont_wght.ttf'); + font-style: italic; +} + +body { + padding: 0; + margin: 0; + font-family: 'OpenSans', sans-serif; + background-color: var(--bg-primary); + color: var(--theme-primary); +} + +a { + color: var(--theme-primary); + text-decoration: none; +} +a:hover { + color: var(--theme-tertiary); + text-decoration: underline; +} + +.app-page-container { + max-width: 600px; + margin-left: auto; + margin-right: auto; + padding: 0.5em; + padding-bottom: 1em; + background-color: var(--bg-page); + + border-bottom-left-radius: 2em; + border-bottom-right-radius: 2em; +} + +.app-page-title { + margin: 0; + font-size: 28px; + font-weight: 500; +} diff --git a/web-app/src/main.ts b/web-app/src/main.ts index fda1e6e..d795bbd 100644 --- a/web-app/src/main.ts +++ b/web-app/src/main.ts @@ -1,12 +1,19 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' +import { library } from '@fortawesome/fontawesome-svg-core' +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' +import { fas } from '@fortawesome/free-solid-svg-icons' +import { far } from '@fortawesome/free-regular-svg-icons' import App from './App.vue' import router from './router' +import './assets/main.css' + +library.add(fas, far) const app = createApp(App) - app.use(createPinia()) app.use(router) +app.component('font-awesome-icon', FontAwesomeIcon) app.mount('#app') diff --git a/web-app/src/pages/HomePage.vue b/web-app/src/pages/HomePage.vue deleted file mode 100644 index f09e275..0000000 --- a/web-app/src/pages/HomePage.vue +++ /dev/null @@ -1,72 +0,0 @@ - - diff --git a/web-app/src/pages/ProfilePage.vue b/web-app/src/pages/ProfilePage.vue new file mode 100644 index 0000000..9ba8ff5 --- /dev/null +++ b/web-app/src/pages/ProfilePage.vue @@ -0,0 +1,51 @@ + + diff --git a/web-app/src/pages/ProfilesPage.vue b/web-app/src/pages/ProfilesPage.vue new file mode 100644 index 0000000..f4b1209 --- /dev/null +++ b/web-app/src/pages/ProfilesPage.vue @@ -0,0 +1,51 @@ + + + diff --git a/web-app/src/pages/UserAccountLayout.vue b/web-app/src/pages/UserAccountLayout.vue new file mode 100644 index 0000000..88cb055 --- /dev/null +++ b/web-app/src/pages/UserAccountLayout.vue @@ -0,0 +1,121 @@ + + + + + + diff --git a/web-app/src/pages/UserHomePage.vue b/web-app/src/pages/UserHomePage.vue new file mode 100644 index 0000000..5546b32 --- /dev/null +++ b/web-app/src/pages/UserHomePage.vue @@ -0,0 +1,13 @@ + + diff --git a/web-app/src/router/index.ts b/web-app/src/router/index.ts index 1ea6394..e990dc2 100644 --- a/web-app/src/router/index.ts +++ b/web-app/src/router/index.ts @@ -1,7 +1,10 @@ -import HomePage from '@/pages/HomePage.vue' +import UserAccountLayout from '@/pages/UserAccountLayout.vue' import LoginPage from '@/pages/LoginPage.vue' +import ProfilePage from '@/pages/ProfilePage.vue' import { useAuthStore } from '@/stores/auth-store' import { createRouter, createWebHistory, type RouteLocationNormalized } from 'vue-router' +import UserHomePage from '@/pages/UserHomePage.vue' +import ProfilesPage from '@/pages/ProfilesPage.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -9,16 +12,45 @@ const router = createRouter({ { path: '/login', component: async () => LoginPage, + meta: { title: 'Login' }, }, { path: '/', - component: async () => HomePage, + component: async () => UserAccountLayout, beforeEnter: onlyAuthenticated, + children: [ + { + path: '', + component: async () => UserHomePage, + meta: { title: 'Home' }, + }, + { + path: 'profiles', + component: async () => ProfilesPage, + meta: { title: 'Profiles' }, + }, + { + path: '/profiles/:name', + component: async () => ProfilePage, + meta: { title: (to: RouteLocationNormalized) => 'Profile ' + to.params.name }, + }, + ], }, ], }) -export function onlyAuthenticated(to: RouteLocationNormalized) { +router.beforeEach((to, _, next) => { + if (to.meta.title !== undefined && typeof to.meta.title === 'string') { + document.title = 'Finnow - ' + to.meta.title + } else if (to.meta.title !== undefined && typeof to.meta.title === 'function') { + document.title = 'Finnow - ' + to.meta.title(to) + } else { + document.title = 'Finnow' + } + next() +}) + +function onlyAuthenticated(to: RouteLocationNormalized) { const authStore = useAuthStore() if (authStore.state) return true if (to.path === '/') return '/login' diff --git a/web-app/src/stores/auth-store.ts b/web-app/src/stores/auth-store.ts index c7e4d06..4b128c2 100644 --- a/web-app/src/stores/auth-store.ts +++ b/web-app/src/stores/auth-store.ts @@ -1,3 +1,4 @@ +import { isExpired, parseSubject } from '@/api/token-util' import { defineStore } from 'pinia' import { ref, type Ref } from 'vue' @@ -7,15 +8,28 @@ export interface AuthenticatedData { } export const useAuthStore = defineStore('auth', () => { - const state: Ref = ref(null) + const state: Ref = ref(getStateFromLocalStorage()) function onUserLoggedIn(username: string, token: string) { state.value = { username, token } + localStorage.setItem('token', token) } function onUserLoggedOut() { state.value = null + localStorage.clear() } return { state, onUserLoggedIn, onUserLoggedOut } }) + +function getStateFromLocalStorage(): AuthenticatedData | null { + const token = localStorage.getItem('token') + if (token === null || token.length === 0 || isExpired(token)) { + localStorage.clear() + return null + } + const username = parseSubject(token) + console.info('Loaded authentication information for user', username) + return { username, token } +}