From f25fe61f38fc13d8b9bc745f67b9526027b6a382 Mon Sep 17 00:00:00 2001 From: "Sakith B." Date: Mon, 18 Aug 2025 14:33:31 +0530 Subject: [PATCH 01/54] ui --- .../src/routes/app/(reader)/+layout.svelte | 106 +++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/app/(reader)/+layout.svelte b/frontend/src/routes/app/(reader)/+layout.svelte index 9e2b077..acc9dcd 100644 --- a/frontend/src/routes/app/(reader)/+layout.svelte +++ b/frontend/src/routes/app/(reader)/+layout.svelte @@ -1,7 +1,8 @@ - -{#if iconUrl} - -{/if} - - diff --git a/frontend/src/lib/actions/collectionsActions.ts b/frontend/src/lib/actions/collectionsActions.ts new file mode 100644 index 0000000..c299ce1 --- /dev/null +++ b/frontend/src/lib/actions/collectionsActions.ts @@ -0,0 +1,19 @@ +import type { Collection } from '$lib/types'; +import api from '../api'; + +export async function getCollections() { + const res = await api.get('/collections'); + return res.collections as Collection[]; +} + +export async function getCollectionBySlug(slug: string) { + const res = await api.get(`/collections/${slug}`); + return res.collection as Collection; +} + +export async function createCollection(name: string, isPublic: boolean) { + const res = await api.post('/collections', { name, is_public: isPublic }); + return res.collection as Collection; +} + + diff --git a/frontend/src/lib/actions/initActions.ts b/frontend/src/lib/actions/initActions.ts new file mode 100644 index 0000000..17ed23f --- /dev/null +++ b/frontend/src/lib/actions/initActions.ts @@ -0,0 +1,11 @@ +import type { Collection } from '$lib/types'; +import api from '../api'; + +export async function init() { + const res = await api.get('/init'); + return { + collections: res.collections as Collection[] + }; +} + + diff --git a/frontend/src/lib/actions/publicationsActions.ts b/frontend/src/lib/actions/publicationsActions.ts new file mode 100644 index 0000000..b5af2b3 --- /dev/null +++ b/frontend/src/lib/actions/publicationsActions.ts @@ -0,0 +1,12 @@ +import type { Publication } from '$lib/types'; +import api from '../api'; + +export async function addPublication(collectionSlug: string, url: string) { + const res = await api.post('/publications', { + collection_slug: collectionSlug, + url + }); + return res.publication as Publication; +} + + diff --git a/frontend/src/routes/app/types.ts b/frontend/src/lib/types.ts similarity index 100% rename from frontend/src/routes/app/types.ts rename to frontend/src/lib/types.ts index a2c28f4..9e9c35b 100644 --- a/frontend/src/routes/app/types.ts +++ b/frontend/src/lib/types.ts @@ -1,4 +1,3 @@ - export interface Collection { id: number; name: string; @@ -39,3 +38,4 @@ export interface Item { word_count?: number; } + diff --git a/frontend/src/routes/app/(reader)/+layout.svelte b/frontend/src/routes/app/(reader)/+layout.svelte index cfa6800..7dbe5f6 100644 --- a/frontend/src/routes/app/(reader)/+layout.svelte +++ b/frontend/src/routes/app/(reader)/+layout.svelte @@ -15,8 +15,9 @@ } from '../appStore'; import { goto } from '$app/navigation'; import { page } from '$app/stores'; - import type { Collection, Publication, Item } from '../types'; + import type { Collection, Publication, Item } from '$lib/types'; import { onMount, tick } from 'svelte'; + import { toast } from '@hyvor/design/components'; import api from '$lib/api'; import ArticleView from '../ArticleView.svelte'; @@ -52,9 +53,9 @@ collectionName = ''; collectionIsPublic = false; goto(`/app/${created.slug}`); - } catch (e) { - console.error('Failed to create collection', e); - } + } catch (e) { + toast.error(e instanceof Error ? e.message : 'Failed to create collection'); + } } function selectPublication(publication?: Publication) { @@ -140,10 +141,10 @@ addingPublication = true; addPublicationError = null; const collectionSlug = $selectedCollection?.slug; - if (!collectionSlug) { - console.error('No collection selected'); - return; - } + if (!collectionSlug) { + toast.error('No collection selected'); + return; + } const res = await api.post('/publications', { collection_slug: collectionSlug, url: value, @@ -154,9 +155,9 @@ } showAddPublicationModal = false; rssUrl = ''; - } catch (e) { - console.error('Failed to add publication', e); - addPublicationError = e instanceof Error ? e.message : 'Failed to add publication'; + } catch (e) { + addPublicationError = e instanceof Error ? e.message : 'Failed to add publication'; + toast.error(addPublicationError); } finally { addingPublication = false; } @@ -169,9 +170,9 @@ try { const res = await api.get('/init'); $collections = res.collections; - } catch (e) { - console.error('Initialization failed', e); - } finally { + } catch (e) { + toast.error(e instanceof Error ? e.message : 'Initialization failed'); + } finally { $loadingInit = false; } }); diff --git a/frontend/src/routes/app/(reader)/[collection_slug]/+layout.svelte b/frontend/src/routes/app/(reader)/[collection_slug]/+layout.svelte index 89d4513..1fb374d 100644 --- a/frontend/src/routes/app/(reader)/[collection_slug]/+layout.svelte +++ b/frontend/src/routes/app/(reader)/[collection_slug]/+layout.svelte @@ -9,6 +9,7 @@ selectedPublication, loadingPublications } from '../../appStore'; + import { toast } from '@hyvor/design/components'; let { children } = $props(); let lastFetchedSlug: string | null = null; @@ -26,7 +27,6 @@ if (!collection || collection.slug === lastFetchedSlug) return; lastFetchedSlug = collection.slug; - console.log("Fetching publications for collection:", collection.slug); loadingPublications.set(true); try { @@ -34,7 +34,7 @@ publications.set(res.publications); selectedPublication.set(null); } catch (e) { - console.error('Failed to fetch publications:', e); + toast.error(e instanceof Error ? e.message : 'Failed to fetch publications'); } finally { loadingPublications.set(false); } diff --git a/frontend/src/routes/app/(reader)/[collection_slug]/[[publication_slug]]/+page.svelte b/frontend/src/routes/app/(reader)/[collection_slug]/[[publication_slug]]/+page.svelte index 124f7db..e117f60 100644 --- a/frontend/src/routes/app/(reader)/[collection_slug]/[[publication_slug]]/+page.svelte +++ b/frontend/src/routes/app/(reader)/[collection_slug]/[[publication_slug]]/+page.svelte @@ -1,6 +1,7 @@ @@ -188,7 +210,13 @@
- { showSidebarMobile = true; }}> + { + showSidebarMobile = true; + }} + >
@@ -196,7 +224,7 @@ {#if $loadingInit} {:else} - + {#snippet trigger()}
{$selectedCollection?.name || 'Select Collection'} @@ -237,66 +265,56 @@
- {#if $loadingPublications} -
- -
- {:else} - - {#each $publications as publication} + {#if $loadingPublications} +
+ +
+ {:else} - {/each} - {/if} + {#each $publications as publication} + + {/each} + {/if}
{#if selectedItem} - {#if item.image} {/if} @@ -380,40 +394,45 @@
-
{ showSidebarMobile = false; }}>
+
{ + showSidebarMobile = false; + }} + >
{ showCreateCollectionModal = false; }} - on:confirm={handleCreateCollection} + bind:show={showCreateCollectionModal} + size="small" + title="Create Collection" + closeOnOutsideClick={true} + closeOnEscape={true} + footer={{ + cancel: { text: 'Cancel', props: { color: 'input' } }, + confirm: { text: 'Create', props: { disabled: !collectionName.trim() } } + }} + on:cancel={() => { + showCreateCollectionModal = false; + }} + on:confirm={handleCreateCollection} > - + Date: Tue, 30 Sep 2025 12:15:35 +0530 Subject: [PATCH 42/54] rebased issue/38 --- backend/.env | 3 ++- frontend/src/routes/app/(reader)/+layout.svelte | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/.env b/backend/.env index 466e446..758771c 100644 --- a/backend/.env +++ b/backend/.env @@ -1,9 +1,10 @@ ###> symfony/framework-bundle ### -APP_SECRET=634272ac4a9ace811b9fe1ab649d2176 +APP_SECRET=Qgg+kph9o0lULpebas4C9xa+tvoNC5b94rCjRf/5gAY= ###< symfony/framework-bundle ### DATABASE_URL="postgresql://postgres:postgres@hyvor-service-pgsql:5432/hyvor_reader?serverVersion=16&charset=utf8" +HYVOR_FAKE=1 ###> symfony/messenger ### MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 diff --git a/frontend/src/routes/app/(reader)/+layout.svelte b/frontend/src/routes/app/(reader)/+layout.svelte index 9e3220d..c665a3d 100644 --- a/frontend/src/routes/app/(reader)/+layout.svelte +++ b/frontend/src/routes/app/(reader)/+layout.svelte @@ -9,6 +9,7 @@ Dropdown, ActionList, ActionListItem, + Divider, Loader, Modal, TextInput, @@ -242,6 +243,7 @@ {/each} +
@@ -550,12 +557,7 @@ min-height: 0; } - .actionlist-divider { - margin: 6px 10px; - height: 1px; - background: var(--border); - border-radius: 1px; - } + .publications-footer { border-top: 1px solid var(--border); diff --git a/frontend/src/routes/app/ArticleView.svelte b/frontend/src/routes/app/ArticleView.svelte index d079b76..267c12b 100644 --- a/frontend/src/routes/app/ArticleView.svelte +++ b/frontend/src/routes/app/ArticleView.svelte @@ -143,7 +143,7 @@ { onPrevious?.(); }} class="nav-button nav-mobile" > @@ -163,7 +163,7 @@ { onNext?.(); }} class="nav-button nav-mobile" > diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a7db909..119383e 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -11,12 +11,7 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "moduleResolution": "Bundler", - "baseUrl": ".", - "paths": { - "$lib": ["src/lib"], - "$lib/*": ["src/lib/*"] - } + "moduleResolution": "Bundler" } // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // From e0e45b4ed2e15982d70f74a50330c1bbca7ef838 Mon Sep 17 00:00:00 2001 From: "Sakith B." Date: Tue, 14 Oct 2025 15:30:02 +0530 Subject: [PATCH 54/54] setup oidc --- backend/config/packages/doctrine.yaml | 6 ++ backend/config/packages/framework.yaml | 8 ++- backend/config/routes/attributes.php | 5 +- backend/config/services.yaml | 5 ++ backend/migrations/Version20250814000000.php | 57 +++++++++++++++++++ .../Authorization/AuthorizationListener.php | 11 +++- frontend/src/lib/api.ts | 10 +++- .../src/routes/app/(reader)/+layout.svelte | 41 +++++++++---- .../(reader)/[collection_slug]/+layout.svelte | 9 ++- .../[[publication_slug]]/+page.svelte | 11 +++- 10 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 backend/migrations/Version20250814000000.php diff --git a/backend/config/packages/doctrine.yaml b/backend/config/packages/doctrine.yaml index e3cecd8..79aca76 100644 --- a/backend/config/packages/doctrine.yaml +++ b/backend/config/packages/doctrine.yaml @@ -26,6 +26,12 @@ doctrine: dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App + InternalBundle: + is_bundle: false + type: attribute + dir: '%kernel.project_dir%/vendor/hyvor/internal/bundle/src/Entity' + prefix: 'Hyvor\Internal\Bundle\Entity' + alias: InternalBundle controller_resolver: auto_mapping: false diff --git a/backend/config/packages/framework.yaml b/backend/config/packages/framework.yaml index 7e1ee1f..0f68b9b 100644 --- a/backend/config/packages/framework.yaml +++ b/backend/config/packages/framework.yaml @@ -3,7 +3,9 @@ framework: secret: '%env(APP_SECRET)%' # Note that the session will be started ONLY if you read or write from it. - session: true + session: + handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler + trusted_proxies: '%env(TRUSTED_PROXIES)%' #esi: true #fragments: true @@ -13,3 +15,7 @@ when@test: test: true session: storage_factory_id: session.storage.factory.mock_file + +when@dev: + framework: + trusted_proxies: '172.16.0.0/12' diff --git a/backend/config/routes/attributes.php b/backend/config/routes/attributes.php index 845d761..8f75fc4 100644 --- a/backend/config/routes/attributes.php +++ b/backend/config/routes/attributes.php @@ -9,6 +9,9 @@ ->prefix('/api/app') ->namePrefix('api_app_'); - // + // OIDC routes + $routes->import('@InternalBundle/src/Controller/OidcController.php', 'attribute') + ->prefix('/api/oidc') + ->namePrefix('api_oidc_'); }; \ No newline at end of file diff --git a/backend/config/services.yaml b/backend/config/services.yaml index 2d6a76f..f13520b 100644 --- a/backend/config/services.yaml +++ b/backend/config/services.yaml @@ -22,3 +22,8 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler: + arguments: + - '%env(DATABASE_URL)%' + - { db_table: 'oidc_sessions' } diff --git a/backend/migrations/Version20250814000000.php b/backend/migrations/Version20250814000000.php new file mode 100644 index 0000000..6ab7e9d --- /dev/null +++ b/backend/migrations/Version20250814000000.php @@ -0,0 +1,57 @@ +addSql(<<<'SQL' + CREATE TABLE oidc_users ( + id SERIAL PRIMARY KEY, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + iss TEXT NOT NULL, + sub TEXT NOT NULL, + email TEXT NOT NULL, + name TEXT NOT NULL, + picture_url TEXT, + website_url TEXT, + UNIQUE (iss, sub) + ) + SQL); + + $this->addSql('CREATE INDEX idx_oidc_users_email ON oidc_users (email)'); + + $this->addSql(<<<'SQL' + CREATE TABLE oidc_sessions ( + sess_id VARCHAR(128) NOT NULL PRIMARY KEY, + sess_data BYTEA NOT NULL, + sess_lifetime INTEGER NOT NULL, + sess_time INTEGER NOT NULL + ) + SQL); + + $this->addSql('CREATE INDEX idx_oidc_sessions_sess_lifetime ON oidc_sessions (sess_lifetime)'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE IF EXISTS oidc_sessions'); + $this->addSql('DROP INDEX IF EXISTS idx_oidc_sessions_sess_lifetime'); + $this->addSql('DROP TABLE IF EXISTS oidc_users'); + $this->addSql('DROP INDEX IF EXISTS idx_oidc_users_email'); + } +} + + diff --git a/backend/src/Api/App/Authorization/AuthorizationListener.php b/backend/src/Api/App/Authorization/AuthorizationListener.php index 454be82..b96409e 100644 --- a/backend/src/Api/App/Authorization/AuthorizationListener.php +++ b/backend/src/Api/App/Authorization/AuthorizationListener.php @@ -7,7 +7,7 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerEvent; -use Symfony\Component\HttpKernel\Exception\HttpException; +use Hyvor\Internal\Bundle\Api\DataCarryingHttpException; use Symfony\Component\HttpKernel\KernelEvents; #[AsEventListener(event: KernelEvents::CONTROLLER, priority: 200)] @@ -40,7 +40,14 @@ public function __invoke(ControllerEvent $event): void $user = $this->auth->check($request); if ($user === false) { - throw new HttpException(401, 'Unauthorized'); + throw new DataCarryingHttpException( + 401, + [ + 'login_url' => $this->auth->authUrl('login'), + 'signup_url' => $this->auth->authUrl('signup'), + ], + 'Unauthorized' + ); } $request->attributes->set(self::RESOLVED_USER_ATTRIBUTE_KEY, $user); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 7655beb..97d0eb8 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -25,8 +25,14 @@ export default class api { }); if (!response.ok) { - const json = await response.json(); - throw new Error(json?.message ?? 'Unknown error'); + let json: any = null; + try { + json = await response.json(); + } catch {} + const err: any = new Error(json?.message ?? 'Unknown error'); + err.code = response.status; + err.data = json?.data ?? null; + throw err; } return await response.json(); diff --git a/frontend/src/routes/app/(reader)/+layout.svelte b/frontend/src/routes/app/(reader)/+layout.svelte index 89b07a6..26a03b6 100644 --- a/frontend/src/routes/app/(reader)/+layout.svelte +++ b/frontend/src/routes/app/(reader)/+layout.svelte @@ -30,7 +30,7 @@ import type { Collection, Publication, Item } from '$lib/types'; import { onMount, tick } from 'svelte'; import { toast } from '@hyvor/design/components'; - import api from '$lib/api'; +import api from '$lib/api'; import ArticleView from '../ArticleView.svelte'; import IconGridFill from '@hyvor/icons/IconGridFill'; @@ -71,9 +71,16 @@ collectionName = ''; collectionIsPublic = false; goto(`/app/${created.slug}`); - } catch (e) { - toast.error(e instanceof Error ? e.message : 'Failed to create collection'); - } + } catch (e) { + if ((e as any)?.code === 401) { + const toPage = $page.url.searchParams.has('signup') ? 'signup' : 'login'; + const url = new URL((e as any)?.data?.[toPage + '_url'], location.origin); + url.searchParams.set('redirect', location.href); + location.href = url.toString(); + } else { + toast.error(e instanceof Error ? e.message : 'Failed to create collection'); + } + } } function selectPublication(publication?: Publication) { @@ -174,9 +181,16 @@ } showAddPublicationModal = false; rssUrl = ''; - } catch (e) { - addPublicationError = e instanceof Error ? e.message : 'Failed to add publication'; - toast.error(addPublicationError); + } catch (e) { + if ((e as any)?.code === 401) { + const toPage = $page.url.searchParams.has('signup') ? 'signup' : 'login'; + const url = new URL((e as any)?.data?.[toPage + '_url'], location.origin); + url.searchParams.set('redirect', location.href); + location.href = url.toString(); + } else { + addPublicationError = e instanceof Error ? e.message : 'Failed to add publication'; + toast.error(addPublicationError); + } } finally { addingPublication = false; } @@ -189,9 +203,16 @@ try { const res = await api.get('/init'); $collections = res.collections; - } catch (e) { - toast.error(e instanceof Error ? e.message : 'Initialization failed'); - } finally { + } catch (e) { + if ((e as any)?.code === 401) { + const toPage = $page.url.searchParams.has('signup') ? 'signup' : 'login'; + const url = new URL((e as any)?.data?.[toPage + '_url'], location.origin); + url.searchParams.set('redirect', location.href); + location.href = url.toString(); + } else { + toast.error(e instanceof Error ? e.message : 'Initialization failed'); + } + } finally { $loadingInit = false; } diff --git a/frontend/src/routes/app/(reader)/[collection_slug]/+layout.svelte b/frontend/src/routes/app/(reader)/[collection_slug]/+layout.svelte index 1fb374d..9783346 100644 --- a/frontend/src/routes/app/(reader)/[collection_slug]/+layout.svelte +++ b/frontend/src/routes/app/(reader)/[collection_slug]/+layout.svelte @@ -34,7 +34,14 @@ publications.set(res.publications); selectedPublication.set(null); } catch (e) { - toast.error(e instanceof Error ? e.message : 'Failed to fetch publications'); + if ((e as any)?.code === 401) { + const toPage = $page.url.searchParams.has('signup') ? 'signup' : 'login'; + const url = new URL((e as any)?.data?.[toPage + '_url'], location.origin); + url.searchParams.set('redirect', location.href); + location.href = url.toString(); + } else { + toast.error(e instanceof Error ? e.message : 'Failed to fetch publications'); + } } finally { loadingPublications.set(false); } diff --git a/frontend/src/routes/app/(reader)/[collection_slug]/[[publication_slug]]/+page.svelte b/frontend/src/routes/app/(reader)/[collection_slug]/[[publication_slug]]/+page.svelte index e117f60..61e28ca 100644 --- a/frontend/src/routes/app/(reader)/[collection_slug]/[[publication_slug]]/+page.svelte +++ b/frontend/src/routes/app/(reader)/[collection_slug]/[[publication_slug]]/+page.svelte @@ -26,8 +26,15 @@ const params = pub ? { publication_slug: pub.slug } : { collection_slug: col.slug }; const res = await api.get('/items', params); items.set(res.items); - } catch (e) { - toast.error(e instanceof Error ? e.message : 'Failed to fetch items'); + } catch (e) { + if ((e as any)?.code === 401) { + const toPage = $page.url.searchParams.has('signup') ? 'signup' : 'login'; + const url = new URL((e as any)?.data?.[toPage + '_url'], location.origin); + url.searchParams.set('redirect', location.href); + location.href = url.toString(); + } else { + toast.error(e instanceof Error ? e.message : 'Failed to fetch items'); + } } finally { loadingItems.set(false); }