-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Transcribe auth #634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1a5e17c
35ffc23
cf4f7c9
3c09f93
35a52df
9e404ca
a51159a
646856b
14791b9
854bacc
f5c91d8
aaa4b8a
c021425
ea25a35
afa3aa6
59378c4
d8bd26a
d609f96
9b847de
039d0b3
12f101b
76360c9
1ef65d4
8b15c4d
0ee9c92
2a026b8
a9ab56b
623be22
0f18855
552f2bc
9b63167
634f091
4f5fa88
0149ae8
1fd8bb3
c43a0e0
38d8a97
3fd4867
f7f09c7
7ab1d18
4849c39
e097994
a6a1f98
3382b4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,4 +56,6 @@ next-env.d.ts | |
| .gemini/ | ||
|
|
||
| # GitHub App credentials | ||
| gha-creds-*.json | ||
| gha-creds-*.json | ||
|
|
||
| /src/server/local.db | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Project create | ||
|
|
||
| ## Pathname | ||
|
|
||
| Create a new component at `src/app/transcribe/create`. | ||
|
|
||
| ## UX | ||
|
|
||
| Add form to create a new project. | ||
|
|
||
| The required schema is defined in `src/server/src/schemata.ts` – `projectCreatePayloadSchema`. | ||
|
|
||
| Fields: | ||
|
|
||
| - Title: text input, required | ||
| - ID: text input, required, unique across all projects | ||
| - isHandwritten: select (unset, handwritten, typed), required | ||
| - Location: coordinates of the settlement relevant to the project, required. May copy `src/app/components/contribute/spatial-input.tsx` | ||
| - Sources: URLs relevant to the project, required. May copy `src/app/components/contribute/sources-input.tsx` | ||
| - Table locale: select (unset, pl, ru, uk), required | ||
| - Years range: range of years relevant to the project, or a single year, required. May copy `src/app/components/contribute/years-input.tsx` | ||
|
|
||
| ## Request | ||
|
|
||
| Send a POST request to `new URL('/api/transcribe/projects', environment.NEXT_PUBLIC_API_SITE)`. | ||
|
|
||
| ### Request body | ||
|
|
||
| ```http | ||
| POST /api/transcribe/projects HTTP/1.1 | ||
| Host: localhost:4000 | ||
| Content-Type: application/json | ||
|
|
||
| { | ||
| "id": "test", | ||
| "isHandwritten": true, | ||
| "location": [0, 0], | ||
| "sources": ["https://example/com/data"], | ||
| "tableLocale": "pl", | ||
| "title": "Test", | ||
| "yearsRange": [1900, 2000] | ||
| } | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Project create link | ||
|
|
||
| Add a link to the project creation page (`/transcribe/create`) at `src/app/transcribe/page.tsx`, above the projects list. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Project create crash | ||
|
|
||
| ## Error text | ||
|
|
||
| ```plain | ||
| TypeError: Cannot read properties of undefined (reading 'join') | ||
| at Object.render (page.tsx:164:38) | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| --- | ||
| description: Update database configuration to support local SQLite fallback for development without Turso credentials. | ||
| status: draft | ||
| targets: | ||
| - src/server/src/database/client.ts | ||
| - src/server/.env.example | ||
| - src/server/src/environment.ts | ||
| - src/server/package.json | ||
| - src/server/src/database/init.ts | ||
| context: | ||
| - src/server/src/database/schema.sql | ||
| --- | ||
|
|
||
| # Local SQLite Fallback for Development | ||
|
|
||
| ## 1. Architectural Boundary | ||
|
|
||
| - **Execution Context:** Server (Hono) | ||
| - **Data Scope:** SQLite (Kysely) | ||
|
|
||
| --- | ||
|
|
||
| ## 2. State Transition Matrix | ||
|
|
||
| ### Fault / Current State | ||
|
|
||
| - **Condition:** A new developer clones the repository and attempts to start the backend locally. | ||
| - **Behavior:** The application crashes during environment variable validation because `TURSO_DATABASE_URL` is enforced as a URL and `TURSO_DATABASE_TOKEN` is a mandatory non-empty string. | ||
| - **Log/Trace:** | ||
|
|
||
| ```ts | ||
| ZodError: [ | ||
| { | ||
| code: 'invalid_type', | ||
| expected: 'string', | ||
| received: 'undefined', | ||
| path: ['TURSO_DATABASE_URL'], | ||
| message: 'Required', | ||
| }, | ||
| { | ||
| code: 'custom', | ||
| message: 'String cannot be empty', | ||
| path: ['TURSO_DATABASE_TOKEN'], | ||
| }, | ||
| ]; | ||
| ``` | ||
|
|
||
| ### Target / Resolved State | ||
|
|
||
| - **Condition:** A new developer starts the application locally with an empty `.env` or defaults from `.env.example`. | ||
| - **Behavior:** The application falls back to a local SQLite database file (e.g., `file:local.db`) and bypasses the requirement for a Turso authentication token. The developer can initialize the local database schema using a new `db:init` script. | ||
| - **Schema/Type Alteration:** | ||
|
|
||
| ```ts | ||
| const environmentSchema = z.object({ | ||
| // ... | ||
| TURSO_DATABASE_URL: z.string().default('file:local.db'), | ||
| TURSO_DATABASE_TOKEN: nonEmptyString.optional(), | ||
| }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 3. Execution Pipeline | ||
|
|
||
| ### 3.1. src/server/src/environment.ts | ||
|
|
||
| 1. Modify the `TURSO_DATABASE_URL` validation in `environmentSchema` to support local file connections (e.g., `z.string()`) and provide a default value of `'file:local.db'`. | ||
| 2. Update `TURSO_DATABASE_TOKEN` to be optional using `.optional()`. | ||
|
|
||
| ### 3.2. src/server/.env.example | ||
|
|
||
| 1. Update the example configuration to indicate that `TURSO_DATABASE_URL` and `TURSO_DATABASE_TOKEN` can be omitted for local development to use a local SQLite instance. | ||
|
|
||
| ### 3.3. src/server/src/database/client.ts | ||
|
|
||
| 1. Update the `createClient` configuration object to safely pass `environment.TURSO_DATABASE_TOKEN` only if it is defined, preventing validation errors from `@libsql/client` when running without a remote auth token. | ||
|
|
||
| ### 3.4. src/server/package.json | ||
|
|
||
| 1. Add a `"db:init"` script (e.g., `"db:init": "tsx src/database/init.ts"`) that developers can run to seed the local SQLite database file with the correct initial schema structure after cloning the project. | ||
|
|
||
| ### 3.5. src/server/src/database/init.ts | ||
|
|
||
| 1. Create a script that utilizes the existing `better-sqlite3` and `fs` packages. | ||
| 2. The script should read the contents of `src/server/src/database/schema.sql`. | ||
| 3. It should instantiate a new `Database('local.db')` and execute the SQL schema string to initialize the database structure. | ||
|
|
||
| --- | ||
|
|
||
| ## 4. Hard Constraints | ||
|
|
||
| - **Backend ESM:** All relative imports in Hono/Node.js files MUST terminate with explicit `.js` extensions. | ||
| - **Isolation:** Do not modify schemas, context files, or unrelated components not explicitly listed in the `targets` frontmatter. | ||
|
|
||
| --- | ||
|
|
||
| ## 5. Agentic Verification | ||
|
|
||
| Execute the following commands to validate the implementation: | ||
|
|
||
| 1. **Type & Lint Pass:** Run standard formatting and type checks. | ||
|
|
||
| ```opencode | ||
| /lint | ||
| ``` | ||
|
|
||
| 2. **Targeted Test Execution:** Run the specific route or backend test. | ||
|
|
||
| ```opencode | ||
| /test-server | ||
| ``` | ||
|
|
||
| 3. **ESM Validation (Backend Only):** | ||
|
|
||
| ```opencode | ||
| /verify-esm src/server/src/database/client.ts | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| .root { | ||
| display: inline-flex; | ||
| padding: 0.25rem 0.5rem; | ||
| background-color: #dc2626; | ||
| color: #fff; | ||
| border-radius: 0.25rem; | ||
| font-size: 0.875rem; | ||
| line-height: 1.25rem; | ||
| font-weight: 500; | ||
| text-decoration: none; | ||
| cursor: pointer; | ||
| user-select: none; | ||
| transition: | ||
| background-color 0.2s ease-in-out, | ||
| border-color 0.2s ease-in-out, | ||
| color 0.2s ease-in-out; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| 'use client'; | ||
|
|
||
| import { googleLogout } from '@react-oauth/google'; | ||
| import { useRouter } from 'next/navigation'; | ||
| import { toast } from 'sonner'; | ||
|
|
||
| import requestApi from '../services/api'; | ||
|
|
||
| import styles from './logout-button.module.css'; | ||
|
|
||
| export default function LogoutButton() { | ||
| const router = useRouter(); | ||
|
|
||
| const handleLogout = () => { | ||
| // 1. Sever the local Google Identity SDK state | ||
| googleLogout(); | ||
|
|
||
| // 2. Execute backend cookie destruction | ||
| requestApi('/api/auth/session/current', { | ||
| method: 'DELETE', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unclear usage of REST standard. Use one of options:
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changing to DELETE |
||
| }) | ||
| .then(() => { | ||
| router.push('/transcribe/login'); | ||
| return; | ||
| }) | ||
| .catch(() => { | ||
| toast.error('Failed to log out'); | ||
| }); | ||
| }; | ||
|
|
||
| return ( | ||
| <button onClick={handleLogout} className={styles.root}> | ||
| Log Out | ||
| </button> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| 'use client'; | ||
|
|
||
| import { useEffect, useState } from 'react'; | ||
| import { toast } from 'sonner'; | ||
|
|
||
| import { type Project, projectResponseSchema } from '../schemata'; | ||
| import requestApi from '../services/api'; | ||
|
|
||
| export default function ProjectsList() { | ||
| const [projects, setProjects] = useState<Project[]>([]); | ||
| useEffect(() => { | ||
| requestApi('/api/transcribe/projects') | ||
| .then((response) => response.json()) | ||
|
undead404 marked this conversation as resolved.
|
||
| .then((data: unknown) => { | ||
| const projectsData = projectResponseSchema.parse(data); | ||
| setProjects(projectsData.projects); | ||
| return; | ||
| }) | ||
| .catch(() => { | ||
| toast.error('Error loading projects'); | ||
| }); | ||
| }, []); | ||
| return ( | ||
| <section> | ||
| <h1>Projects</h1> | ||
| {projects.map((project) => ( | ||
| <p key={project.id}>{project.title}</p> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's expected to render not ? just not sure about logic, but looks like it's a place where you render list of projects, and potentially can open one of them
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just a placeholder for now; the implementation is the next stage – see #694 |
||
| ))} | ||
| {projects.length === 0 && <p>No projects</p>} | ||
| </section> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| .root { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| padding: 0 1rem; | ||
| background-color: #fff; | ||
| border-bottom: 1px solid #e5e7eb; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.