refactor: fix table and clean up auth code
This commit is contained in:
@@ -32,7 +32,13 @@ await esbuild.build({
|
|||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
"@opentui/react",
|
"@opentui/react",
|
||||||
"@opentui/react/jsx-runtime",
|
"@opentui/react/jsx-runtime",
|
||||||
|
"effect",
|
||||||
|
"@effect/platform",
|
||||||
|
"@effect/platform-bun",
|
||||||
"bun:ffi",
|
"bun:ffi",
|
||||||
|
"@rocicorp/zero",
|
||||||
|
"better-auth",
|
||||||
|
"zod",
|
||||||
// "./assets/**/*.scm",
|
// "./assets/**/*.scm",
|
||||||
// "./assets/**/*.wasm",
|
// "./assets/**/*.wasm",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import { Context, Data, Effect, Layer, Schema, Console, Schedule, Match, Ref, Duration } from "effect";
|
import { Context, Data, Effect, Layer, Schema, Console, Schedule, Ref, Duration } from "effect";
|
||||||
import { FileSystem } from "@effect/platform";
|
import { FileSystem } from "@effect/platform";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
import { AuthState } from "./schema";
|
import { AuthState } from "./schema";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import type { BetterFetchResponse } from "@better-fetch/fetch";
|
import type { BetterFetchResponse } from "@better-fetch/fetch";
|
||||||
|
|
||||||
const CLIENT_ID = "koon-family";
|
class AuthClientUnknownError extends Data.TaggedError("AuthClientUnknownError") {};
|
||||||
|
class AuthClientExpiredToken extends Data.TaggedError("AuthClientExpiredToken") {};
|
||||||
const getFromFromDisk = Effect.gen(function* () {
|
class AuthClientNoData extends Data.TaggedError("AuthClientNoData") {};
|
||||||
const fs = yield* FileSystem.FileSystem;
|
class AuthClientFetchError extends Data.TaggedError("AuthClientFetchError")<{ message: string, }> {};
|
||||||
const content = yield* fs.readFileString(config.authPath);
|
class AuthClientError<T> extends Data.TaggedError("AuthClientError")<{
|
||||||
const auth = yield* Schema.decode(Schema.parseJson(AuthState))(content);
|
error: T,
|
||||||
if (auth.session.expiresAt < new Date()) yield* Effect.fail("Token expired");
|
|
||||||
return auth;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
class AuthClientErrorString extends Data.TaggedError("AuthClientErrorString")<{
|
|
||||||
errorString: string,
|
|
||||||
}> {};
|
}> {};
|
||||||
|
|
||||||
|
|
||||||
type ErrorType<E> = { [key in keyof ((E extends Record<string, any> ? E : {
|
type ErrorType<E> = { [key in keyof ((E extends Record<string, any> ? E : {
|
||||||
message?: string;
|
message?: string;
|
||||||
}) & {
|
}) & {
|
||||||
@@ -33,16 +25,12 @@ type ErrorType<E> = { [key in keyof ((E extends Record<string, any> ? E : {
|
|||||||
statusText: string;
|
statusText: string;
|
||||||
})[key]; };
|
})[key]; };
|
||||||
|
|
||||||
class AuthClientError<T> extends Data.TaggedError("AuthClientError")<{
|
|
||||||
error: T,
|
|
||||||
}> {};
|
|
||||||
|
|
||||||
export class AuthClient extends Context.Tag("AuthClient")<AuthClient, AuthClientImpl>() {};
|
export class AuthClient extends Context.Tag("AuthClient")<AuthClient, AuthClientImpl>() {};
|
||||||
|
|
||||||
export interface AuthClientImpl {
|
export interface AuthClientImpl {
|
||||||
use: <T, E>(
|
use: <T, E>(
|
||||||
fn: (client: typeof authClient) => Promise<BetterFetchResponse<T, E>>,
|
fn: (client: typeof authClient) => Promise<BetterFetchResponse<T, E>>,
|
||||||
) => Effect.Effect<T, AuthClientError<ErrorType<E>> | AuthClientErrorString, never>
|
) => Effect.Effect<T, AuthClientError<ErrorType<E>> | AuthClientFetchError | AuthClientUnknownError | AuthClientNoData, never>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -53,16 +41,18 @@ export const make = () =>
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { data, error } = yield* Effect.tryPromise({
|
const { data, error } = yield* Effect.tryPromise({
|
||||||
try: () => fn(authClient),
|
try: () => fn(authClient),
|
||||||
catch: () => new AuthClientErrorString({ errorString: "Bad" }),
|
catch: (error) => error instanceof Error
|
||||||
|
? new AuthClientFetchError({ message: error.message })
|
||||||
|
: new AuthClientUnknownError()
|
||||||
});
|
});
|
||||||
if (error != null) return yield* Effect.fail(new AuthClientError({ error }));
|
if (error != null) return yield* Effect.fail(new AuthClientError({ error }));
|
||||||
if (data == null) return yield* Effect.fail(new AuthClientErrorString({ errorString: "No data" }));
|
if (data == null) return yield* Effect.fail(new AuthClientNoData());
|
||||||
return data;
|
return data;
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
export const layer = () => Layer.scoped(AuthClient, make());
|
export const AuthClientLayer = Layer.scoped(AuthClient, make());
|
||||||
|
|
||||||
const pollToken = ({ device_code }: { device_code: string }) => Effect.gen(function* () {
|
const pollToken = ({ device_code }: { device_code: string }) => Effect.gen(function* () {
|
||||||
const auth = yield* AuthClient;
|
const auth = yield* AuthClient;
|
||||||
@@ -74,8 +64,8 @@ const pollToken = ({ device_code }: { device_code: string }) => Effect.gen(funct
|
|||||||
return client.device.token({
|
return client.device.token({
|
||||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
device_code,
|
device_code,
|
||||||
client_id: CLIENT_ID,
|
client_id: config.authClientId,
|
||||||
fetchOptions: { headers: { "user-agent": "CLI" } },
|
fetchOptions: { headers: { "user-agent": config.authClientUserAgent } },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -104,10 +94,18 @@ const pollToken = ({ device_code }: { device_code: string }) => Effect.gen(funct
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getFromFromDisk = Effect.gen(function* () {
|
||||||
|
const fs = yield* FileSystem.FileSystem;
|
||||||
|
const content = yield* fs.readFileString(config.authPath);
|
||||||
|
const auth = yield* Schema.decode(Schema.parseJson(AuthState))(content);
|
||||||
|
if (auth.session.expiresAt < new Date()) yield* Effect.fail(new AuthClientExpiredToken());
|
||||||
|
return auth;
|
||||||
|
});
|
||||||
|
|
||||||
const requestAuth = Effect.gen(function* () {
|
const requestAuth = Effect.gen(function* () {
|
||||||
const auth = yield* AuthClient;
|
const auth = yield* AuthClient;
|
||||||
const { device_code, user_code } = yield* auth.use(client => client.device.code({
|
const { device_code, user_code } = yield* auth.use(client => client.device.code({
|
||||||
client_id: CLIENT_ID,
|
client_id: config.authClientId,
|
||||||
scope: "openid profile email",
|
scope: "openid profile email",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -123,7 +121,7 @@ const requestAuth = Effect.gen(function* () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
if (sessionData == null) return yield* Effect.fail("Session was null");
|
if (sessionData == null) return yield* Effect.fail(new AuthClientNoData());
|
||||||
|
|
||||||
const result = yield* Schema.decodeUnknown(AuthState)(sessionData)
|
const result = yield* Schema.decodeUnknown(AuthState)(sessionData)
|
||||||
|
|
||||||
@@ -135,6 +133,34 @@ const requestAuth = Effect.gen(function* () {
|
|||||||
|
|
||||||
export const getAuth = Effect.gen(function* () {
|
export const getAuth = Effect.gen(function* () {
|
||||||
return yield* getFromFromDisk.pipe(
|
return yield* getFromFromDisk.pipe(
|
||||||
Effect.catchAll(() => requestAuth)
|
Effect.catchAll(() => requestAuth),
|
||||||
|
Effect.catchTag("AuthClientFetchError", (err) => Effect.gen(function* () {
|
||||||
|
yield* Console.error("Authentication failed: " + err.message);
|
||||||
|
process.exit(1);
|
||||||
|
})),
|
||||||
|
Effect.catchTag("AuthClientNoData", () => Effect.gen(function* () {
|
||||||
|
yield* Console.error("Authentication failed: No error and no data was given by the auth server.");
|
||||||
|
process.exit(1);
|
||||||
|
})),
|
||||||
|
Effect.catchTag("ParseError", (err) => Effect.gen(function* () {
|
||||||
|
yield* Console.error("Authentication failed: Auth data failed: " + err.toString());
|
||||||
|
process.exit(1);
|
||||||
|
})),
|
||||||
|
Effect.catchTag("BadArgument", () => Effect.gen(function* () {
|
||||||
|
yield* Console.error("Authentication failed: Bad argument");
|
||||||
|
process.exit(1);
|
||||||
|
})),
|
||||||
|
Effect.catchTag("SystemError", () => Effect.gen(function* () {
|
||||||
|
yield* Console.error("Authentication failed: System error");
|
||||||
|
process.exit(1);
|
||||||
|
})),
|
||||||
|
Effect.catchTag("AuthClientError", ({ error }) => Effect.gen(function* () {
|
||||||
|
yield* Console.error("Authentication error: " + error.statusText);
|
||||||
|
process.exit(1);
|
||||||
|
})),
|
||||||
|
Effect.catchTag("AuthClientUnknownError", () => Effect.gen(function* () {
|
||||||
|
yield* Console.error("Unknown authentication error");
|
||||||
|
process.exit(1);
|
||||||
|
})),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const AUTH_PATH = join(PATH, "auth.json");
|
|||||||
export const config = {
|
export const config = {
|
||||||
dir: PATH,
|
dir: PATH,
|
||||||
authPath: AUTH_PATH,
|
authPath: AUTH_PATH,
|
||||||
|
authClientId: "koon-family",
|
||||||
|
authClientUserAgent: "CLI",
|
||||||
zeroUrl: "http://laptop:4848",
|
zeroUrl: "http://laptop:4848",
|
||||||
apiUrl: "http://laptop:3000"
|
apiUrl: "http://laptop:3000"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { App, type Route } from "@money/ui";
|
|||||||
import { ZeroProvider } from "@rocicorp/zero/react";
|
import { ZeroProvider } from "@rocicorp/zero/react";
|
||||||
import { schema } from '@money/shared';
|
import { schema } from '@money/shared';
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { getAuth, layer } from "./auth";
|
import { AuthClientLayer, getAuth } from "./auth";
|
||||||
import { Effect } from "effect";
|
import { Effect } from "effect";
|
||||||
import { BunContext } from "@effect/platform-bun";
|
import { BunContext } from "@effect/platform-bun";
|
||||||
import type { AuthData } from "./schema";
|
import type { AuthData } from "./schema";
|
||||||
@@ -21,7 +21,7 @@ function Main({ auth }: { auth: AuthData }) {
|
|||||||
return (
|
return (
|
||||||
<ZeroProvider {...{ userID: auth.user.id, auth: auth.session.token, server: config.zeroUrl, schema, kvStore }}>
|
<ZeroProvider {...{ userID: auth.user.id, auth: auth.session.token, server: config.zeroUrl, schema, kvStore }}>
|
||||||
<App
|
<App
|
||||||
auth={auth || null}
|
auth={auth}
|
||||||
route={route}
|
route={route}
|
||||||
setRoute={setRoute}
|
setRoute={setRoute}
|
||||||
/>
|
/>
|
||||||
@@ -34,7 +34,7 @@ function Main({ auth }: { auth: AuthData }) {
|
|||||||
const auth = await Effect.runPromise(
|
const auth = await Effect.runPromise(
|
||||||
getAuth.pipe(
|
getAuth.pipe(
|
||||||
Effect.provide(BunContext.layer),
|
Effect.provide(BunContext.layer),
|
||||||
Effect.provide(layer()),
|
Effect.provide(AuthClientLayer),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const renderer = await createCliRenderer({ exitOnCtrlC: false });
|
const renderer = await createCliRenderer({ exitOnCtrlC: false });
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Schema } from "effect";
|
import { Schema } from "effect";
|
||||||
|
|
||||||
|
const DateFromDateOrString = Schema.Union(Schema.DateFromString, Schema.DateFromSelf);
|
||||||
|
|
||||||
const SessionSchema = Schema.Struct({
|
const SessionSchema = Schema.Struct({
|
||||||
expiresAt: Schema.DateFromString,
|
expiresAt: DateFromDateOrString,
|
||||||
token: Schema.String,
|
token: Schema.String,
|
||||||
createdAt: Schema.DateFromString,
|
createdAt: DateFromDateOrString,
|
||||||
updatedAt: Schema.DateFromString,
|
updatedAt: DateFromDateOrString,
|
||||||
ipAddress: Schema.optional(Schema.NullishOr(Schema.String)),
|
ipAddress: Schema.optional(Schema.NullishOr(Schema.String)),
|
||||||
userAgent: Schema.optional(Schema.NullishOr(Schema.String)),
|
userAgent: Schema.optional(Schema.NullishOr(Schema.String)),
|
||||||
userId: Schema.String,
|
userId: Schema.String,
|
||||||
@@ -16,8 +18,8 @@ const UserSchema = Schema.Struct({
|
|||||||
email: Schema.String,
|
email: Schema.String,
|
||||||
emailVerified: Schema.Boolean,
|
emailVerified: Schema.Boolean,
|
||||||
image: Schema.optional(Schema.NullishOr(Schema.String)),
|
image: Schema.optional(Schema.NullishOr(Schema.String)),
|
||||||
createdAt: Schema.DateFromString,
|
createdAt: DateFromDateOrString,
|
||||||
updatedAt: Schema.DateFromString,
|
updatedAt: DateFromDateOrString,
|
||||||
id: Schema.String,
|
id: Schema.String,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { ViewProps, TextProps, PressableProps } from "react-native";
|
import type { ViewProps, TextProps, PressableProps, ScrollViewProps } from "react-native";
|
||||||
|
|
||||||
export function View({ children, style }: ViewProps) {
|
export function View({ children, style }: ViewProps) {
|
||||||
const bg = style &&
|
const bg = style &&
|
||||||
@@ -20,8 +20,26 @@ export function View({ children, style }: ViewProps) {
|
|||||||
? style.flex
|
? style.flex
|
||||||
: undefined
|
: undefined
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const flexShrink = style &&
|
||||||
|
'flexShrink' in style
|
||||||
|
? typeof style.flexShrink == 'number'
|
||||||
|
? style.flexShrink
|
||||||
|
: undefined
|
||||||
|
: undefined;
|
||||||
|
const overflow = style &&
|
||||||
|
'overflow' in style
|
||||||
|
? typeof style.overflow == 'string'
|
||||||
|
? style.overflow
|
||||||
|
: undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return <box backgroundColor={bg} flexDirection={flexDirection} flexGrow={flex}>{children}</box>
|
return <box
|
||||||
|
backgroundColor={bg}
|
||||||
|
flexDirection={flexDirection}
|
||||||
|
flexGrow={flex}
|
||||||
|
overflow={overflow}
|
||||||
|
flexShrink={flexShrink}
|
||||||
|
>{children}</box>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Pressable({ children: childrenRaw, style, onPress }: PressableProps) {
|
export function Pressable({ children: childrenRaw, style, onPress }: PressableProps) {
|
||||||
@@ -69,6 +87,10 @@ export function Text({ style, children }: TextProps) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ScrollView({ children }: ScrollViewProps) {
|
||||||
|
return <scrollbox >{children}</scrollbox>
|
||||||
|
}
|
||||||
|
|
||||||
export const Platform = {
|
export const Platform = {
|
||||||
OS: "tui",
|
OS: "tui",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createContext, use, useState, type ReactNode } from "react";
|
import { createContext, use, useState, type ReactNode } from "react";
|
||||||
import { View, Text } from "react-native";
|
import { View, Text, ScrollView } from "react-native";
|
||||||
import { useKeyboard } from "./useKeyboard";
|
import { useKeyboard } from "./useKeyboard";
|
||||||
|
|
||||||
const HEADER_COLOR = '#7158e2';
|
const HEADER_COLOR = '#7158e2';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as Table from "./table";
|
|||||||
import { useQuery } from "@rocicorp/zero/react";
|
import { useQuery } from "@rocicorp/zero/react";
|
||||||
import { queries, type Transaction } from '@money/shared';
|
import { queries, type Transaction } from '@money/shared';
|
||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
import { View, Text } from "react-native";
|
import { View, Text, ScrollView } from "react-native";
|
||||||
import { RouterContext } from ".";
|
import { RouterContext } from ".";
|
||||||
|
|
||||||
|
|
||||||
@@ -28,13 +28,15 @@ export function Transactions() {
|
|||||||
const [items] = useQuery(queries.allTransactions(auth));
|
const [items] = useQuery(queries.allTransactions(auth));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Provider
|
<Table.Provider data={items} columns={COLUMNS}>
|
||||||
data={items}
|
<View style={{ flex: 1 }}>
|
||||||
columns={COLUMNS} >
|
<View style={{ flexShrink: 0}}>
|
||||||
<Table.Body />
|
<Table.Body />
|
||||||
{/* Spacer */}
|
</View>
|
||||||
<View style={{ flex: 1 }} />
|
</View>
|
||||||
|
<View style={{ flexShrink: 0 }}>
|
||||||
<Selected />
|
<Selected />
|
||||||
|
</View>
|
||||||
</Table.Provider>
|
</Table.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user