@korajs/cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +1609 -643
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1086 -139
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-CMSX76KM.js → chunk-3WNFM3QB.js} +103 -96
- package/dist/chunk-3WNFM3QB.js.map +1 -0
- package/dist/{chunk-MVP5PDT4.js → chunk-PSRM56B7.js} +2 -2
- package/dist/{chunk-YGVO4POI.js → chunk-SOTZIWIF.js} +27 -15
- package/dist/chunk-SOTZIWIF.js.map +1 -0
- package/dist/create.cjs +54 -47
- package/dist/create.cjs.map +1 -1
- package/dist/create.js +2 -2
- package/dist/index.cjs +78 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -2
- package/package.json +9 -3
- package/templates/react-basic/README.md.hbs +10 -1
- package/templates/react-basic/src/App.tsx +112 -126
- package/templates/react-basic/src/index.css +165 -159
- package/templates/react-basic/src/kora-worker.ts +0 -1
- package/templates/react-basic/src/main.tsx +12 -12
- package/templates/react-basic/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-basic/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-basic/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-basic/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-basic/src/schema.ts +6 -12
- package/templates/react-basic/src/vite-env.d.ts +17 -0
- package/templates/react-basic/{tsconfig.json → tsconfig.json.hbs} +1 -2
- package/templates/react-basic/vite.config.ts +57 -50
- package/templates/react-sync/.env.example +19 -0
- package/templates/react-sync/README.md.hbs +43 -1
- package/templates/react-sync/package.json.hbs +3 -0
- package/templates/react-sync/server.ts +90 -26
- package/templates/react-sync/src/App.tsx +132 -130
- package/templates/react-sync/src/auth.ts +25 -0
- package/templates/react-sync/src/index.css +212 -165
- package/templates/react-sync/src/kora-worker.ts +0 -1
- package/templates/react-sync/src/main.tsx +34 -18
- package/templates/react-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-sync/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-sync/src/schema.ts +6 -12
- package/templates/react-sync/src/vite-env.d.ts +17 -0
- package/templates/react-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
- package/templates/react-sync/vite.config.ts +67 -60
- package/templates/react-tailwind/README.md.hbs +10 -1
- package/templates/react-tailwind/src/App.tsx +186 -197
- package/templates/react-tailwind/src/index.css +3 -3
- package/templates/react-tailwind/src/kora-worker.ts +0 -1
- package/templates/react-tailwind/src/main.tsx +19 -12
- package/templates/react-tailwind/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-tailwind/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-tailwind/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-tailwind/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-tailwind/src/schema.ts +6 -12
- package/templates/react-tailwind/src/vite-env.d.ts +17 -0
- package/templates/react-tailwind/{tsconfig.json → tsconfig.json.hbs} +1 -2
- package/templates/react-tailwind/vite.config.ts +57 -50
- package/templates/react-tailwind-sync/.env.example +19 -0
- package/templates/react-tailwind-sync/README.md.hbs +43 -1
- package/templates/react-tailwind-sync/package.json.hbs +3 -0
- package/templates/react-tailwind-sync/server.ts +90 -26
- package/templates/react-tailwind-sync/src/App.tsx +246 -222
- package/templates/react-tailwind-sync/src/auth.ts +25 -0
- package/templates/react-tailwind-sync/src/index.css +3 -3
- package/templates/react-tailwind-sync/src/kora-worker.ts +0 -1
- package/templates/react-tailwind-sync/src/main.tsx +48 -18
- package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
- package/templates/react-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
- package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +31 -0
- package/templates/react-tailwind-sync/src/schema.ts +6 -12
- package/templates/react-tailwind-sync/src/vite-env.d.ts +17 -0
- package/templates/react-tailwind-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
- package/templates/react-tailwind-sync/vite.config.ts +67 -60
- package/templates/tauri-react/.env.example +20 -0
- package/templates/tauri-react/README.md.hbs +36 -1
- package/templates/tauri-react/dev.ts +11 -11
- package/templates/tauri-react/package.json.hbs +2 -0
- package/templates/tauri-react/server.ts +86 -12
- package/templates/tauri-react/src/App.tsx +769 -544
- package/templates/tauri-react/src/AppShell.tsx +125 -83
- package/templates/tauri-react/src/SetupScreen.tsx +257 -208
- package/templates/tauri-react/src/auth.ts +45 -0
- package/templates/tauri-react/src/main.tsx +3 -3
- package/templates/tauri-react/src/modules/todos/todo.mutations.ts +25 -0
- package/templates/tauri-react/src/modules/todos/todo.queries.ts +5 -0
- package/templates/tauri-react/src/modules/todos/todo.schema.ts +10 -0
- package/templates/tauri-react/src/modules/todos/useTodos.ts +31 -0
- package/templates/tauri-react/src/schema.ts +6 -12
- package/templates/tauri-react/src/sync-config.ts +47 -47
- package/templates/tauri-react/src/updater.ts +15 -15
- package/templates/tauri-react/src/vite-env.d.ts +17 -0
- package/templates/tauri-react/src-tauri/tauri.conf.json +1 -1
- package/templates/tauri-react/tsconfig.json.hbs +14 -0
- package/templates/tauri-react/vite.config.ts +9 -9
- package/dist/chunk-CMSX76KM.js.map +0 -1
- package/dist/chunk-YGVO4POI.js.map +0 -1
- package/templates/tauri-react/tsconfig.json +0 -15
- /package/dist/{chunk-MVP5PDT4.js.map → chunk-PSRM56B7.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readdirSync
|
|
2
|
-
import {
|
|
1
|
+
import { copyFileSync, existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
3
|
import react from '@vitejs/plugin-react'
|
|
4
4
|
import type { Plugin } from 'vite'
|
|
5
5
|
import { defineConfig } from 'vite'
|
|
@@ -10,23 +10,23 @@ import { defineConfig } from 'vite'
|
|
|
10
10
|
* cannot be used and SQLite WASM falls back to in-memory storage (no persistence).
|
|
11
11
|
*/
|
|
12
12
|
function crossOriginIsolation(): Plugin {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
return {
|
|
14
|
+
name: 'cross-origin-isolation',
|
|
15
|
+
configureServer(server) {
|
|
16
|
+
server.middlewares.use((_req, res, next) => {
|
|
17
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
|
|
18
|
+
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
|
|
19
|
+
next()
|
|
20
|
+
})
|
|
21
|
+
},
|
|
22
|
+
configurePreviewServer(server) {
|
|
23
|
+
server.middlewares.use((_req, res, next) => {
|
|
24
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
|
|
25
|
+
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
|
|
26
|
+
next()
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -36,40 +36,47 @@ function crossOriginIsolation(): Plugin {
|
|
|
36
36
|
* sqlite3 loads it dynamically) so the full OPFS VFS can initialize without errors
|
|
37
37
|
*/
|
|
38
38
|
function sqliteWasmHotfix(): Plugin {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
return {
|
|
40
|
+
name: 'sqlite-wasm-hotfix',
|
|
41
|
+
apply: 'build',
|
|
42
|
+
closeBundle() {
|
|
43
|
+
const assetsDir = resolve('dist', 'assets')
|
|
44
|
+
if (!existsSync(assetsDir)) return
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
// Copy hashed sqlite3.wasm to unhashed name
|
|
47
|
+
for (const file of readdirSync(assetsDir)) {
|
|
48
|
+
if (/^sqlite3-.+\.wasm$/.test(file)) {
|
|
49
|
+
copyFileSync(join(assetsDir, file), join(assetsDir, 'sqlite3.wasm'))
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
// Copy OPFS async proxy worker (dynamically loaded by sqlite3, not detected by Vite)
|
|
55
|
+
const proxyFile = resolve(
|
|
56
|
+
'node_modules',
|
|
57
|
+
'@sqlite.org',
|
|
58
|
+
'sqlite-wasm',
|
|
59
|
+
'sqlite-wasm',
|
|
60
|
+
'jswasm',
|
|
61
|
+
'sqlite3-opfs-async-proxy.js',
|
|
62
|
+
)
|
|
63
|
+
if (existsSync(proxyFile)) {
|
|
64
|
+
copyFileSync(proxyFile, join(assetsDir, 'sqlite3-opfs-async-proxy.js'))
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export default defineConfig({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
plugins: [react(), crossOriginIsolation(), sqliteWasmHotfix()],
|
|
72
|
+
worker: {
|
|
73
|
+
format: 'es',
|
|
74
|
+
},
|
|
75
|
+
optimizeDeps: {
|
|
76
|
+
exclude: ['@sqlite.org/sqlite-wasm', '@korajs/store'],
|
|
77
|
+
include: ['yjs'],
|
|
78
|
+
},
|
|
79
|
+
resolve: {
|
|
80
|
+
dedupe: ['yjs'],
|
|
81
|
+
},
|
|
75
82
|
})
|
|
@@ -4,3 +4,22 @@ VITE_SYNC_URL=ws://localhost:3001
|
|
|
4
4
|
|
|
5
5
|
# Sync server port
|
|
6
6
|
PORT=3001
|
|
7
|
+
|
|
8
|
+
# Server database
|
|
9
|
+
# Leave DATABASE_URL empty to use SQLite at KORA_SERVER_DB.
|
|
10
|
+
KORA_SERVER_DB=./kora-server.db
|
|
11
|
+
DATABASE_URL=
|
|
12
|
+
|
|
13
|
+
# Operational endpoints (/__kora, /__kora/status, /__kora/events, /__kora/metrics, backups)
|
|
14
|
+
# Set these in production. Use Authorization: Bearer <token> when calling protected endpoints.
|
|
15
|
+
KORA_ADMIN_TOKEN=
|
|
16
|
+
KORA_METRICS_TOKEN=
|
|
17
|
+
KORA_BACKUP_TOKEN=
|
|
18
|
+
|
|
19
|
+
# Optional sync path
|
|
20
|
+
KORA_SYNC_PATH=/kora-sync
|
|
21
|
+
|
|
22
|
+
# Auth
|
|
23
|
+
# Set this to enable built-in /auth/* routes and authenticated sync.
|
|
24
|
+
# Generate with: node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
|
|
25
|
+
KORA_AUTH_SECRET=
|
|
@@ -17,21 +17,63 @@ This starts both the Vite dev server and the Kora sync server.
|
|
|
17
17
|
|
|
18
18
|
```
|
|
19
19
|
src/
|
|
20
|
-
schema.ts #
|
|
20
|
+
schema.ts # Schema entry point
|
|
21
|
+
auth.ts # Auth client and OAuth callback handling
|
|
21
22
|
main.tsx # App initialization and Kora setup
|
|
22
23
|
App.tsx # Main application component
|
|
24
|
+
modules/
|
|
25
|
+
todos/
|
|
26
|
+
todo.schema.ts # Todo collection definition
|
|
27
|
+
todo.queries.ts # Reusable todo query builders
|
|
28
|
+
todo.mutations.ts # Todo mutation helpers
|
|
29
|
+
useTodos.ts # React hook connecting todos to the UI
|
|
23
30
|
kora-worker.ts # SQLite WASM web worker
|
|
24
31
|
index.css # Styles
|
|
25
32
|
server.ts # Kora sync server
|
|
26
33
|
kora.config.ts # Kora configuration
|
|
27
34
|
```
|
|
28
35
|
|
|
36
|
+
In a feature module, `schema` defines data shape, `queries` contain reads only, `mutations` contain
|
|
37
|
+
writes, and `useTodos.ts` is the React binding that exposes todo data and actions to components.
|
|
38
|
+
|
|
29
39
|
## Environment Variables
|
|
30
40
|
|
|
31
41
|
| Variable | Default | Description |
|
|
32
42
|
|----------|---------|-------------|
|
|
33
43
|
| `VITE_SYNC_URL` | `ws://localhost:3001` | WebSocket URL for the sync server |
|
|
44
|
+
| `VITE_AUTH_URL` | current origin | HTTP URL for `/auth/*` routes |
|
|
34
45
|
| `PORT` | `3001` | Sync server port |
|
|
46
|
+
| `KORA_SERVER_DB` | `./kora-server.db` | SQLite file used by the sync server when `DATABASE_URL` is empty |
|
|
47
|
+
| `KORA_AUTH_DB` | `./kora-auth.db` | SQLite file used by OAuth state and linked identities when `DATABASE_URL` is empty |
|
|
48
|
+
| `DATABASE_URL` | (none) | PostgreSQL connection string for production sync server deployments |
|
|
49
|
+
| `KORA_SYNC_PATH` | `/kora-sync` | WebSocket path exposed by the sync server |
|
|
50
|
+
| `KORA_AUTH_SECRET` | (none) | Enables built-in `/auth/*` routes and authenticated sync when set |
|
|
51
|
+
| `KORA_GOOGLE_CLIENT_ID` | (none) | Enables Google OAuth when set with `KORA_GOOGLE_REDIRECT_URI` |
|
|
52
|
+
| `KORA_GOOGLE_CLIENT_SECRET` | (none) | Google OAuth client secret for confidential web clients |
|
|
53
|
+
| `KORA_GOOGLE_REDIRECT_URI` | (none) | OAuth callback URL, usually your app URL that handles `code` and `state` |
|
|
54
|
+
| `KORA_OAUTH_PKCE` | `true` when no client secret | Enables PKCE for public desktop/mobile OAuth clients |
|
|
55
|
+
| `KORA_ADMIN_TOKEN` | (none) | Protects `/__kora`, `/__kora/status`, and `/__kora/events` |
|
|
56
|
+
| `KORA_METRICS_TOKEN` | (none) | Protects `/__kora/metrics` |
|
|
57
|
+
| `KORA_BACKUP_TOKEN` | (none) | Protects server backup and restore endpoints |
|
|
58
|
+
|
|
59
|
+
## Authentication
|
|
60
|
+
|
|
61
|
+
Set `KORA_AUTH_SECRET` to enable built-in auth on the sync server. Generate a secret with:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The template already creates `src/auth.ts`, wraps the app in `AuthProvider`, and wires sync with `createKoraAuthSync({ authClient, schema })`. To enable Google OAuth, set:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
KORA_AUTH_SECRET=...
|
|
71
|
+
KORA_GOOGLE_CLIENT_ID=...
|
|
72
|
+
KORA_GOOGLE_CLIENT_SECRET=...
|
|
73
|
+
KORA_GOOGLE_REDIRECT_URI=http://localhost:5173
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Use `authClient.signInWithOAuth('google')` for web redirect sign-in, or `authClient.getOAuthAuthorizationUrl('google')` plus `authClient.completeOAuthSignIn('google', { code, state })` for custom handoff flows. See the [Authentication Guide](https://ehoneahobed.github.io/kora/guide/authentication).
|
|
35
77
|
|
|
36
78
|
## Scripts
|
|
37
79
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"korajs": "{{koraVersion}}",
|
|
15
|
+
"@korajs/auth": "{{koraVersion}}",
|
|
15
16
|
"@korajs/react": "{{koraVersion}}",
|
|
16
17
|
"@korajs/store": "{{koraVersion}}",
|
|
17
18
|
"@sqlite.org/sqlite-wasm": ">=3.51.0-build1",
|
|
@@ -34,6 +35,8 @@
|
|
|
34
35
|
"@types/react-dom": "^19.0.0",
|
|
35
36
|
"@types/ws": "^8.5.0",
|
|
36
37
|
"@vitejs/plugin-react": "^4.3.0",
|
|
38
|
+
"better-sqlite3": "^12.9.0",
|
|
39
|
+
"postgres": "^3.4.0",
|
|
37
40
|
"tsx": "^4.19.0",
|
|
38
41
|
"typescript": "^5.7.0",
|
|
39
42
|
"vite": "^6.0.0"
|
|
@@ -1,26 +1,90 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
import {
|
|
2
|
+
type CreateKoraAuthServerOptions,
|
|
3
|
+
createKoraAuthServer,
|
|
4
|
+
createPostgresOAuthStores,
|
|
5
|
+
createSqliteOAuthStores,
|
|
6
|
+
googleProvider,
|
|
7
|
+
} from '@korajs/auth/server'
|
|
8
|
+
import {
|
|
9
|
+
createPostgresServerStore,
|
|
10
|
+
createProductionServer,
|
|
11
|
+
createSqliteServerStore,
|
|
12
|
+
} from '@korajs/server'
|
|
13
|
+
import schema from './src/schema'
|
|
14
|
+
|
|
15
|
+
async function createStore() {
|
|
16
|
+
if (process.env.DATABASE_URL) {
|
|
17
|
+
return createPostgresServerStore({
|
|
18
|
+
connectionString: process.env.DATABASE_URL,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return createSqliteServerStore({
|
|
23
|
+
filename: process.env.KORA_SERVER_DB || './kora-server.db',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function start() {
|
|
28
|
+
const store = await createStore()
|
|
29
|
+
await store.setSchema(schema)
|
|
30
|
+
|
|
31
|
+
const syncPath = process.env.KORA_SYNC_PATH || '/kora-sync'
|
|
32
|
+
const auth = await createAuth()
|
|
33
|
+
const server = createProductionServer({
|
|
34
|
+
store,
|
|
35
|
+
port: Number(process.env.PORT) || 3001,
|
|
36
|
+
staticDir: './dist',
|
|
37
|
+
syncPath,
|
|
38
|
+
httpRoutes: auth ? [{ path: '/auth', handle: auth.handleRequest }] : undefined,
|
|
39
|
+
syncOptions: auth ? { auth: auth.auth } : undefined,
|
|
40
|
+
operationalAuth: {
|
|
41
|
+
adminToken: process.env.KORA_ADMIN_TOKEN,
|
|
42
|
+
metricsToken: process.env.KORA_METRICS_TOKEN,
|
|
43
|
+
backupToken: process.env.KORA_BACKUP_TOKEN,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const url = await server.start()
|
|
48
|
+
console.log(`Kora app running at ${url}`)
|
|
49
|
+
console.log(` Sync endpoint: ${url.replace('http', 'ws')}${syncPath}`)
|
|
50
|
+
if (auth) console.log(` Auth endpoint: ${url}/auth`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
void start()
|
|
54
|
+
|
|
55
|
+
async function createAuth() {
|
|
56
|
+
if (!process.env.KORA_AUTH_SECRET) {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const oauth = await createOAuthConfig()
|
|
61
|
+
return createKoraAuthServer({
|
|
62
|
+
jwtSecret: process.env.KORA_AUTH_SECRET,
|
|
63
|
+
...(oauth ? { oauth } : {}),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function createOAuthConfig(): Promise<CreateKoraAuthServerOptions['oauth'] | undefined> {
|
|
68
|
+
if (!process.env.KORA_GOOGLE_CLIENT_ID || !process.env.KORA_GOOGLE_REDIRECT_URI) {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const oauthStores = process.env.DATABASE_URL
|
|
73
|
+
? await createPostgresOAuthStores({ connectionString: process.env.DATABASE_URL })
|
|
74
|
+
: await createSqliteOAuthStores({
|
|
75
|
+
filename: process.env.KORA_AUTH_DB || './kora-auth.db',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
providers: [
|
|
80
|
+
googleProvider({
|
|
81
|
+
clientId: process.env.KORA_GOOGLE_CLIENT_ID,
|
|
82
|
+
clientSecret: process.env.KORA_GOOGLE_CLIENT_SECRET,
|
|
83
|
+
redirectUri: process.env.KORA_GOOGLE_REDIRECT_URI,
|
|
84
|
+
pkce: process.env.KORA_OAUTH_PKCE === 'true' || !process.env.KORA_GOOGLE_CLIENT_SECRET,
|
|
85
|
+
}),
|
|
86
|
+
],
|
|
87
|
+
stateStore: oauthStores.stateStore,
|
|
88
|
+
linkedIdentityStore: oauthStores.linkedIdentityStore,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -1,150 +1,152 @@
|
|
|
1
|
+
import { useAuth } from '@korajs/auth/react'
|
|
2
|
+
import { useSyncStatus } from '@korajs/react'
|
|
1
3
|
import { useState } from 'react'
|
|
2
|
-
import {
|
|
4
|
+
import { useTodos } from './modules/todos/useTodos'
|
|
3
5
|
|
|
4
6
|
type Filter = 'all' | 'active' | 'completed'
|
|
5
7
|
|
|
6
8
|
export function App() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
(data: { title: string }) => todos.insert(data)
|
|
11
|
-
)
|
|
12
|
-
const { mutate: toggleTodo } = useMutation(
|
|
13
|
-
(id: string, data: { completed: boolean }) => todos.update(id, data)
|
|
14
|
-
)
|
|
15
|
-
const { mutate: deleteTodo } = useMutation(
|
|
16
|
-
(id: string) => todos.delete(id)
|
|
17
|
-
)
|
|
18
|
-
const status = useSyncStatus()
|
|
9
|
+
const { allTodos, activeTodos, completedTodos, addTodo, toggleTodo, deleteTodo } = useTodos()
|
|
10
|
+
const status = useSyncStatus()
|
|
11
|
+
const { user, isAuthenticated, signInWithOAuth, signOut, error } = useAuth()
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
const [filter, setFilter] = useState<Filter>('all')
|
|
14
|
+
const [input, setInput] = useState('')
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const filteredTodos =
|
|
26
|
-
filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
|
|
16
|
+
const filteredTodos =
|
|
17
|
+
filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
19
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
20
|
+
e.preventDefault()
|
|
21
|
+
const title = input.trim()
|
|
22
|
+
if (title) {
|
|
23
|
+
addTodo.mutate({ title })
|
|
24
|
+
setInput('')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
const clearCompleted = () => {
|
|
29
|
+
for (const todo of completedTodos) {
|
|
30
|
+
deleteTodo.mutate(todo.id)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
return (
|
|
35
|
+
<div className="app">
|
|
36
|
+
<div className="header">
|
|
37
|
+
<h1>My Tasks</h1>
|
|
38
|
+
<div className="header-actions">
|
|
39
|
+
<div className="sync-badge">
|
|
40
|
+
<span className={`sync-dot ${status.status}`} />
|
|
41
|
+
<span>{status.status}</span>
|
|
42
|
+
</div>
|
|
43
|
+
{isAuthenticated ? (
|
|
44
|
+
<button type="button" className="auth-button" onClick={() => signOut()}>
|
|
45
|
+
{user?.email || 'Sign out'}
|
|
46
|
+
</button>
|
|
47
|
+
) : (
|
|
48
|
+
<button type="button" className="auth-button" onClick={() => signInWithOAuth('google')}>
|
|
49
|
+
Sign in
|
|
50
|
+
</button>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
<div className="stat-card">
|
|
55
|
-
<div className="label">Total</div>
|
|
56
|
-
<div className="value muted">{allTodos.length}</div>
|
|
57
|
-
</div>
|
|
58
|
-
<div className="stat-card">
|
|
59
|
-
<div className="label">Remaining</div>
|
|
60
|
-
<div className="value warning">{activeTodos.length}</div>
|
|
61
|
-
</div>
|
|
62
|
-
<div className="stat-card">
|
|
63
|
-
<div className="label">Done</div>
|
|
64
|
-
<div className="value success">{completedTodos.length}</div>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
55
|
+
{error && <div className="auth-error">{error}</div>}
|
|
67
56
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
57
|
+
<div className="stats">
|
|
58
|
+
<div className="stat-card">
|
|
59
|
+
<div className="label">Total</div>
|
|
60
|
+
<div className="value muted">{allTodos.length}</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="stat-card">
|
|
63
|
+
<div className="label">Remaining</div>
|
|
64
|
+
<div className="value warning">{activeTodos.length}</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="stat-card">
|
|
67
|
+
<div className="label">Done</div>
|
|
68
|
+
<div className="value success">{completedTodos.length}</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
</button>
|
|
92
|
-
)
|
|
93
|
-
})}
|
|
94
|
-
</div>
|
|
72
|
+
<form className="add-form" onSubmit={handleSubmit}>
|
|
73
|
+
<input
|
|
74
|
+
type="text"
|
|
75
|
+
value={input}
|
|
76
|
+
onChange={(e) => setInput(e.target.value)}
|
|
77
|
+
placeholder="What needs to be done?"
|
|
78
|
+
/>
|
|
79
|
+
<button type="submit" disabled={!input.trim()}>
|
|
80
|
+
Add
|
|
81
|
+
</button>
|
|
82
|
+
</form>
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<span className="time">{formatTime(Number(todo.createdAt))}</span>
|
|
117
|
-
)}
|
|
118
|
-
<button
|
|
119
|
-
className="delete-btn"
|
|
120
|
-
onClick={() => deleteTodo(todo.id)}
|
|
121
|
-
>
|
|
122
|
-
\u00d7
|
|
123
|
-
</button>
|
|
124
|
-
</div>
|
|
125
|
-
))
|
|
126
|
-
)}
|
|
127
|
-
</div>
|
|
84
|
+
<div className="filters">
|
|
85
|
+
{(['all', 'active', 'completed'] as const).map((f) => {
|
|
86
|
+
const count =
|
|
87
|
+
f === 'all'
|
|
88
|
+
? allTodos.length
|
|
89
|
+
: f === 'active'
|
|
90
|
+
? activeTodos.length
|
|
91
|
+
: completedTodos.length
|
|
92
|
+
return (
|
|
93
|
+
<button
|
|
94
|
+
key={f}
|
|
95
|
+
className={`filter-btn ${filter === f ? 'active' : ''}`}
|
|
96
|
+
onClick={() => setFilter(f)}
|
|
97
|
+
>
|
|
98
|
+
{f.charAt(0).toUpperCase() + f.slice(1)}
|
|
99
|
+
<span className="badge">{count}</span>
|
|
100
|
+
</button>
|
|
101
|
+
)
|
|
102
|
+
})}
|
|
103
|
+
</div>
|
|
128
104
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
105
|
+
<div className="todo-list">
|
|
106
|
+
{filteredTodos.length === 0 ? (
|
|
107
|
+
<div className="empty-state">
|
|
108
|
+
{filter === 'all' && 'No tasks yet. Add one above!'}
|
|
109
|
+
{filter === 'active' && 'All caught up! No active tasks.'}
|
|
110
|
+
{filter === 'completed' && 'No completed tasks yet.'}
|
|
111
|
+
</div>
|
|
112
|
+
) : (
|
|
113
|
+
filteredTodos.map((todo) => (
|
|
114
|
+
<div key={todo.id} className="todo-item">
|
|
115
|
+
<button
|
|
116
|
+
className={`toggle ${todo.completed ? 'checked' : ''}`}
|
|
117
|
+
onClick={() => toggleTodo.mutate(todo.id, { completed: !todo.completed })}
|
|
118
|
+
>
|
|
119
|
+
{todo.completed ? '\u2713' : ''}
|
|
120
|
+
</button>
|
|
121
|
+
<span className={`title ${todo.completed ? 'done' : ''}`}>{String(todo.title)}</span>
|
|
122
|
+
{todo.createdAt && <span className="time">{formatTime(Number(todo.createdAt))}</span>}
|
|
123
|
+
<button className="delete-btn" onClick={() => deleteTodo.mutate(todo.id)}>
|
|
124
|
+
\u00d7
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
))
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
137
130
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
{allTodos.length > 0 && (
|
|
132
|
+
<div className="footer">
|
|
133
|
+
<span>
|
|
134
|
+
{activeTodos.length} item{activeTodos.length !== 1 ? 's' : ''} left
|
|
135
|
+
</span>
|
|
136
|
+
{completedTodos.length > 0 && <button onClick={clearCompleted}>Clear completed</button>}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
<p className="branding">Powered by Kora — offline-first, real-time sync</p>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
function formatTime(timestamp: number): string {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
const date = new Date(timestamp)
|
|
147
|
+
const now = new Date()
|
|
148
|
+
if (date.toDateString() === now.toDateString()) {
|
|
149
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
150
|
+
}
|
|
151
|
+
return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
150
152
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createKoraAuth } from '@korajs/auth'
|
|
2
|
+
|
|
3
|
+
export const authServerUrl =
|
|
4
|
+
import.meta.env.VITE_AUTH_URL || `${window.location.protocol}//${window.location.host}`
|
|
5
|
+
|
|
6
|
+
export const authClient = createKoraAuth({
|
|
7
|
+
serverUrl: authServerUrl,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export async function completeOAuthCallbackFromLocation(): Promise<void> {
|
|
11
|
+
const url = new URL(window.location.href)
|
|
12
|
+
const code = url.searchParams.get('code')
|
|
13
|
+
const state = url.searchParams.get('state')
|
|
14
|
+
const provider = url.searchParams.get('provider') || 'google'
|
|
15
|
+
|
|
16
|
+
if (!code || !state) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await authClient.completeOAuthSignIn(provider, { code, state })
|
|
21
|
+
url.searchParams.delete('code')
|
|
22
|
+
url.searchParams.delete('state')
|
|
23
|
+
url.searchParams.delete('provider')
|
|
24
|
+
window.history.replaceState({}, document.title, `${url.pathname}${url.search}${url.hash}`)
|
|
25
|
+
}
|