mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
✨ feat: Add auth flow (#146)
This adds a simple way to incorporate a centralized authentication flow. The idea is to have the user, API and SSH (for machine authentication) all in one place using `openauthjs` + `SST` We also have a database now :) > We are using InstantDB as it allows us to authenticate a use with just the email. Plus it is super simple simple to use _of course after the initial fumbles trying to design the db and relationships_
This commit is contained in:
@@ -1,73 +0,0 @@
|
||||
// image-brightness-analyzer.js
|
||||
|
||||
export class ImageBrightnessAnalyzer {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D ;
|
||||
|
||||
constructor() {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
analyze(imgElement: HTMLImageElement) {
|
||||
if (!(imgElement instanceof HTMLImageElement)) {
|
||||
throw new Error('Input must be an HTMLImageElement');
|
||||
}
|
||||
|
||||
this.canvas.width = imgElement.width;
|
||||
this.canvas.height = imgElement.height;
|
||||
this.ctx.drawImage(imgElement, 0, 0);
|
||||
|
||||
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
let brightestPixel = { value: 0, x: 0, y: 0 };
|
||||
let dullestPixel = { value: 765, x: 0, y: 0 }; // 765 is the max value (255 * 3)
|
||||
|
||||
for (let y = 0; y < this.canvas.height; y++) {
|
||||
for (let x = 0; x < this.canvas.width; x++) {
|
||||
const index = (y * this.canvas.width + x) * 4;
|
||||
const brightness = data[index] + data[index + 1] + data[index + 2];
|
||||
|
||||
if (brightness > brightestPixel.value) {
|
||||
brightestPixel = { value: brightness, x, y };
|
||||
}
|
||||
if (brightness < dullestPixel.value) {
|
||||
dullestPixel = { value: brightness, x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
brightest: {
|
||||
x: brightestPixel.x,
|
||||
y: brightestPixel.y,
|
||||
color: this.getPixelColor(data, brightestPixel.x, brightestPixel.y)
|
||||
},
|
||||
dullest: {
|
||||
x: dullestPixel.x,
|
||||
y: dullestPixel.y,
|
||||
color: this.getPixelColor(data, dullestPixel.x, dullestPixel.y)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getPixelColor(data: any[] | Uint8ClampedArray, x: number, y: number) {
|
||||
const index = (y * this.canvas.width + x) * 4;
|
||||
return {
|
||||
r: data[index],
|
||||
g: data[index + 1],
|
||||
b: data[index + 2]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// // Export the class for use in browser environments
|
||||
// if (typeof window !== 'undefined') {
|
||||
// window.ImageBrightnessAnalyzer = ImageBrightnessAnalyzer;
|
||||
// }
|
||||
|
||||
// // Export for module environments (if using a bundler)
|
||||
// if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
// module.exports = ImageBrightnessAnalyzer;
|
||||
// }
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./image-brightness-analyzer.ts"
|
||||
35
packages/core/instant.perms.ts
Normal file
35
packages/core/instant.perms.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Docs: https://www.instantdb.com/docs/permissions
|
||||
|
||||
import type { InstantRules } from "@instantdb/core";
|
||||
|
||||
const rules = {
|
||||
/**
|
||||
* Welcome to Instant's permission system!
|
||||
* Right now your rules are empty. To start filling them in, check out the docs:
|
||||
* https://www.instantdb.com/docs/permissions
|
||||
*
|
||||
* Here's an example to give you a feel:
|
||||
* posts: {
|
||||
* allow: {
|
||||
* view: "true",
|
||||
* create: "isOwner",
|
||||
* update: "isOwner",
|
||||
* delete: "isOwner",
|
||||
* },
|
||||
* bind: ["isOwner", "auth.id != null && auth.id == data.ownerId"],
|
||||
* },
|
||||
*/
|
||||
"$default": {
|
||||
"allow": {
|
||||
"$default": "false"
|
||||
}
|
||||
},
|
||||
machines: {
|
||||
allow: {
|
||||
"$default": "isOwner",
|
||||
},
|
||||
bind: ["isOwner", "auth.id != null && auth.id == data.ownerID"],
|
||||
}
|
||||
} satisfies InstantRules;
|
||||
|
||||
export default rules;
|
||||
80
packages/core/instant.schema.ts
Normal file
80
packages/core/instant.schema.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { i } from "@instantdb/core";
|
||||
|
||||
const _schema = i.schema({
|
||||
// This section lets you define entities: think `posts`, `comments`, etc
|
||||
// Take a look at the docs to learn more:
|
||||
// https://www.instantdb.com/docs/modeling-data#2-attributes
|
||||
entities: {
|
||||
$users: i.entity({
|
||||
email: i.string().unique().indexed(),
|
||||
}),
|
||||
// This is here because the $users entity has no more than 1 property; email
|
||||
// profiles: i.entity({
|
||||
// name: i.string(),
|
||||
// location: i.string(),
|
||||
// createdAt: i.date(),
|
||||
// deletedAt: i.date().optional()
|
||||
// }),
|
||||
machines: i.entity({
|
||||
hostname: i.string(),
|
||||
location: i.string(),
|
||||
fingerprint: i.string().indexed(),
|
||||
createdAt: i.date(),
|
||||
deletedAt: i.date().optional().indexed()
|
||||
}),
|
||||
// teams: i.entity({
|
||||
// name: i.string(),
|
||||
// type: i.string(), // "Personal" or "Family"
|
||||
// createdAt: i.date(),
|
||||
// deletedAt: i.date().optional()
|
||||
// }),
|
||||
// subscriptions: i.entity({
|
||||
// quantity: i.number(),
|
||||
// polarOrderID: i.string(),
|
||||
// frequency: i.string(),
|
||||
// next: i.date().optional(),
|
||||
// }),
|
||||
// productVariants: i.entity({
|
||||
// name: i.string(),
|
||||
// price: i.number()
|
||||
// })
|
||||
},
|
||||
// links: {
|
||||
// userProfiles: {
|
||||
// forward: { on: 'profiles', has: 'one', label: 'owner' },
|
||||
// reverse: { on: '$users', has: 'one', label: 'profile' },
|
||||
// },
|
||||
// machineOwners: {
|
||||
// forward: { on: 'machines', has: 'one', label: 'owner' },
|
||||
// reverse: { on: '$users', has: 'many', label: 'machinesOwned' },
|
||||
// },
|
||||
// machineTeams: {
|
||||
// forward: { on: 'machines', has: 'one', label: 'team' },
|
||||
// reverse: { on: 'teams', has: 'many', label: 'machines' },
|
||||
// },
|
||||
// userTeams: {
|
||||
// forward: { on: 'teams', has: 'one', label: 'owner' },
|
||||
// reverse: { on: '$users', has: 'many', label: 'teamsOwned' },
|
||||
// },
|
||||
// teamMembers: {
|
||||
// forward: { on: 'teams', has: 'many', label: 'members' },
|
||||
// reverse: { on: '$users', has: 'many', label: 'teams' },
|
||||
// },
|
||||
// subscribedProduct: {
|
||||
// forward: { on: "subscriptions", has: "one", label: "productVariant" },
|
||||
// reverse: { on: "productVariants", has: "many", label: "subscriptions" }
|
||||
// },
|
||||
// subscribedUser: {
|
||||
// forward: { on: "subscriptions", has: "one", label: "owner" },
|
||||
// reverse: { on: "$users", has: "many", label: "subscriptions" }
|
||||
// }
|
||||
// }
|
||||
});
|
||||
|
||||
// This helps Typescript display nicer intellisense
|
||||
type _AppSchema = typeof _schema;
|
||||
interface AppSchema extends _AppSchema { }
|
||||
const schema: AppSchema = _schema;
|
||||
|
||||
export type { AppSchema };
|
||||
export default schema;
|
||||
@@ -1,13 +1,20 @@
|
||||
{
|
||||
"name": "@nestri/core",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"exports":{
|
||||
".":"./index.ts"
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"wrangler": "^3.57.2"
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"loops": "^3.4.1",
|
||||
"ulid": "^2.3.0",
|
||||
"uuid": "^11.0.3",
|
||||
"zod": "^3.24.1",
|
||||
"zod-openapi": "^4.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@instantdb/admin": "^0.17.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
85
packages/core/src/actor.ts
Normal file
85
packages/core/src/actor.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { createContext } from "./context";
|
||||
import { VisibleError } from "./error";
|
||||
|
||||
export interface UserActor {
|
||||
type: "user";
|
||||
properties: {
|
||||
accessToken: string;
|
||||
userID: string;
|
||||
auth?:
|
||||
| {
|
||||
type: "personal";
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: "oauth";
|
||||
clientID: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeviceActor {
|
||||
type: "device";
|
||||
properties: {
|
||||
fingerprint: string;
|
||||
id: string;
|
||||
auth?:
|
||||
| {
|
||||
type: "personal";
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: "oauth";
|
||||
clientID: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface PublicActor {
|
||||
type: "public";
|
||||
properties: {};
|
||||
}
|
||||
|
||||
type Actor = UserActor | PublicActor | DeviceActor;
|
||||
export const ActorContext = createContext<Actor>();
|
||||
|
||||
export function useCurrentUser() {
|
||||
const actor = ActorContext.use();
|
||||
if (actor.type === "user") return {
|
||||
id:actor.properties.userID,
|
||||
token: actor.properties.accessToken
|
||||
};
|
||||
throw new VisibleError(
|
||||
"auth",
|
||||
"unauthorized",
|
||||
`You don't have permission to access this resource`,
|
||||
);
|
||||
}
|
||||
|
||||
export function useCurrentDevice() {
|
||||
const actor = ActorContext.use();
|
||||
if (actor.type === "device") return {
|
||||
fingerprint:actor.properties.fingerprint,
|
||||
id: actor.properties.id
|
||||
};
|
||||
throw new VisibleError(
|
||||
"auth",
|
||||
"unauthorized",
|
||||
`You don't have permission to access this resource`,
|
||||
);
|
||||
}
|
||||
|
||||
export function useActor() {
|
||||
try {
|
||||
return ActorContext.use();
|
||||
} catch {
|
||||
return { type: "public", properties: {} } as PublicActor;
|
||||
}
|
||||
}
|
||||
|
||||
export function assertActor<T extends Actor["type"]>(type: T) {
|
||||
const actor = useActor();
|
||||
if (actor.type !== type)
|
||||
throw new VisibleError("auth", "actor.invalid", `Actor is not "${type}"`);
|
||||
return actor as Extract<Actor, { type: T }>;
|
||||
}
|
||||
7
packages/core/src/common.ts
Normal file
7
packages/core/src/common.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import "zod-openapi/extend";
|
||||
|
||||
export module Common {
|
||||
export const IdDescription = `Unique object identifier.
|
||||
The format and length of IDs may change over time.`;
|
||||
}
|
||||
17
packages/core/src/context.ts
Normal file
17
packages/core/src/context.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
export function createContext<T>() {
|
||||
const storage = new AsyncLocalStorage<T>();
|
||||
return {
|
||||
use() {
|
||||
const result = storage.getStore();
|
||||
if (!result) {
|
||||
throw new Error("No context available");
|
||||
}
|
||||
return result;
|
||||
},
|
||||
with<R>(value: T, fn: () => R) {
|
||||
return storage.run<R>(value, fn);
|
||||
},
|
||||
};
|
||||
}
|
||||
12
packages/core/src/database.ts
Normal file
12
packages/core/src/database.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Resource } from "sst";
|
||||
import { init } from "@instantdb/admin";
|
||||
import schema from "../instant.schema";
|
||||
|
||||
const databaseClient = () => init({
|
||||
appId: Resource.InstantAppId.value,
|
||||
adminToken: Resource.InstantAdminToken.value,
|
||||
schema
|
||||
})
|
||||
|
||||
|
||||
export default databaseClient
|
||||
25
packages/core/src/email/index.ts
Normal file
25
packages/core/src/email/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { LoopsClient } from "loops";
|
||||
import { Resource } from "sst/resource"
|
||||
export namespace Email {
|
||||
export const Client = () => new LoopsClient(Resource.LoopsApiKey.value);
|
||||
|
||||
export async function send(
|
||||
to: string,
|
||||
body: string,
|
||||
) {
|
||||
|
||||
try {
|
||||
await Client().sendTransactionalEmail(
|
||||
{
|
||||
transactionalId: "cm58pdf8d03upb5ecirnmvrfb",
|
||||
email: to,
|
||||
dataVariables: {
|
||||
logincode: body
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("error sending email", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/core/src/error.ts
Normal file
9
packages/core/src/error.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class VisibleError extends Error {
|
||||
constructor(
|
||||
public kind: "input" | "auth",
|
||||
public code: string,
|
||||
public message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
45
packages/core/src/examples.ts
Normal file
45
packages/core/src/examples.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export module Examples {
|
||||
|
||||
export const User = {
|
||||
id: "0bfcc712-df13-4454-81a8-fbee66eddca4",
|
||||
email: "john@example.com",
|
||||
};
|
||||
|
||||
export const Machine = {
|
||||
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||
hostname: "desktopeuo8vsf",
|
||||
fingerprint: "fc27f428f9ca47d4b41b70889ae0c62090",
|
||||
location: "KE, AF"
|
||||
}
|
||||
|
||||
// export const Team = {
|
||||
// id: createID(),
|
||||
// name: "Jane's Family",
|
||||
// type: "Family"
|
||||
// }
|
||||
|
||||
// export const ProductVariant = {
|
||||
// id: createID(),
|
||||
// name: "FamilySM",
|
||||
// price: 10,
|
||||
// };
|
||||
|
||||
// export const Product = {
|
||||
// id: createID(),
|
||||
// name: "Family",
|
||||
// description: "The ideal subscription tier for dedicated gamers who crave more flexibility and social gaming experiences.",
|
||||
// variants: [ProductVariant],
|
||||
// subscription: "allowed" as const,
|
||||
// };
|
||||
|
||||
// export const Subscription = {
|
||||
// id: createID(),
|
||||
// productVariant: ProductVariant,
|
||||
// quantity: 1,
|
||||
// polarOrderID: createID(),
|
||||
// frequency: "monthly" as const,
|
||||
// next: new Date("2024-02-01 19:36:19.000").getTime(),
|
||||
// owner: User
|
||||
// };
|
||||
|
||||
}
|
||||
140
packages/core/src/machine/index.ts
Normal file
140
packages/core/src/machine/index.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { z } from "zod"
|
||||
import { fn } from "../utils";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import databaseClient from "../database"
|
||||
import { useCurrentUser } from "../actor";
|
||||
import { id as createID } from "@instantdb/admin";
|
||||
|
||||
export module Machine {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Machine.id,
|
||||
}),
|
||||
hostname: z.string().openapi({
|
||||
description: "Hostname of the machine",
|
||||
example: Examples.Machine.hostname,
|
||||
}),
|
||||
fingerprint: z.string().openapi({
|
||||
description: "The machine's fingerprint, derived from the machine's Linux machine ID.",
|
||||
example: Examples.Machine.fingerprint,
|
||||
}),
|
||||
location: z.string().openapi({
|
||||
description: "The machine's approximate location; country and continent.",
|
||||
example: Examples.Machine.location,
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Machine",
|
||||
description: "A machine running on the Nestri network.",
|
||||
example: Examples.Machine,
|
||||
});
|
||||
|
||||
export const create = fn(z.object({
|
||||
fingerprint: z.string(),
|
||||
hostname: z.string(),
|
||||
location: z.string()
|
||||
}), async (input) => {
|
||||
const id = createID()
|
||||
const now = new Date().getTime()
|
||||
const db = databaseClient()
|
||||
await db.transact(
|
||||
db.tx.machines[id]!.update({
|
||||
fingerprint: input.fingerprint,
|
||||
hostname: input.hostname,
|
||||
location: input.location,
|
||||
createdAt: now,
|
||||
})
|
||||
)
|
||||
|
||||
return id
|
||||
})
|
||||
|
||||
export const remove = fn(z.string(), async (id) => {
|
||||
const now = new Date().getTime()
|
||||
// const device = useCurrentDevice()
|
||||
// const db = databaseClient()
|
||||
|
||||
// if (device.id) { // the machine can delete itself
|
||||
// await db.transact(db.tx.machines[device.id]!.update({ deletedAt: now }))
|
||||
// } else {// the user can delete it manually
|
||||
const user = useCurrentUser()
|
||||
const db = databaseClient().asUser({ token: user.token })
|
||||
await db.transact(db.tx.machines[id]!.update({ deletedAt: now }))
|
||||
// }
|
||||
|
||||
return "ok"
|
||||
})
|
||||
|
||||
export const fromID = fn(z.string(), async (id) => {
|
||||
const user = useCurrentUser()
|
||||
const db = databaseClient().asUser({ token: user.token })
|
||||
|
||||
const query = {
|
||||
machines: {
|
||||
$: {
|
||||
where: {
|
||||
id: id,
|
||||
deletedAt: { $isNull: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
return res.machines[0]
|
||||
})
|
||||
|
||||
export const fromFingerprint = fn(z.string(), async (input) => {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
machines: {
|
||||
$: {
|
||||
where: {
|
||||
fingerprint: input,
|
||||
deletedAt: { $isNull: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
return res.machines[0]
|
||||
})
|
||||
|
||||
export const list = async () => {
|
||||
const user = useCurrentUser()
|
||||
const db = databaseClient().asUser({ token: user.token })
|
||||
|
||||
const query = {
|
||||
$users: {
|
||||
$: { where: { id: user.id } },
|
||||
machines: {
|
||||
$: {
|
||||
deletedAt: { $isNull: true }
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
return res.$users[0]?.machines
|
||||
}
|
||||
|
||||
export const link = fn(z.object({
|
||||
machineId: z.string()
|
||||
}), async (input) => {
|
||||
const user = useCurrentUser()
|
||||
const db = databaseClient()
|
||||
|
||||
await db.transact(db.tx.machines[input.machineId]!.link({ owner: user.id }))
|
||||
|
||||
return "ok"
|
||||
})
|
||||
}
|
||||
48
packages/core/src/team/index.ts
Normal file
48
packages/core/src/team/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import databaseClient from "../database"
|
||||
import { z } from "zod"
|
||||
import { Common } from "../common";
|
||||
import { createID, fn } from "../utils";
|
||||
import { Examples } from "../examples";
|
||||
|
||||
export module Team {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Team.id,
|
||||
}),
|
||||
name: z.string().openapi({
|
||||
description: "Name of the machine",
|
||||
example: Examples.Team.name,
|
||||
}),
|
||||
type: z.string().nullable().openapi({
|
||||
description: "Whether this is a personal or family type of team",
|
||||
example: Examples.Team.type,
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Team",
|
||||
description: "A group of Nestri user's who share the same machine",
|
||||
example: Examples.Team,
|
||||
});
|
||||
|
||||
export const create = fn(z.object({
|
||||
name: z.string(),
|
||||
type: z.enum(["personal", "family"]),
|
||||
owner: z.string(),
|
||||
}), async (input) => {
|
||||
const id = createID("machine")
|
||||
const now = new Date().getTime()
|
||||
const db = databaseClient()
|
||||
|
||||
await db.transact(db.tx.teams[id]!.update({
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
createdAt: now
|
||||
}).link({
|
||||
owner: input.owner,
|
||||
}))
|
||||
|
||||
return id
|
||||
})
|
||||
}
|
||||
17
packages/core/src/types.ts
Normal file
17
packages/core/src/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface CloudflareCF {
|
||||
colo: string;
|
||||
continent: string;
|
||||
country: string,
|
||||
city: string;
|
||||
region: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
metroCode: string;
|
||||
postalCode: string;
|
||||
timezone: string;
|
||||
regionCode: number;
|
||||
}
|
||||
|
||||
export interface CFRequest extends Request {
|
||||
cf: CloudflareCF
|
||||
}
|
||||
37
packages/core/src/user/index.ts
Normal file
37
packages/core/src/user/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
import databaseClient from "../database"
|
||||
import { fn } from "../utils";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
|
||||
export module User {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.User.id,
|
||||
}),
|
||||
email: z.string().nullable().openapi({
|
||||
description: "Email address of the user.",
|
||||
example: Examples.User.email,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "User",
|
||||
description: "A Nestri console user.",
|
||||
example: Examples.User,
|
||||
});
|
||||
|
||||
export const fromEmail = fn(z.string(), async (email) => {
|
||||
const db = databaseClient()
|
||||
const res = await db.auth.getUser({ email })
|
||||
return res
|
||||
})
|
||||
|
||||
export const create = fn(z.string(), async (email) => {
|
||||
const db = databaseClient()
|
||||
const token = await db.auth.createToken(email)
|
||||
|
||||
return token
|
||||
})
|
||||
}
|
||||
13
packages/core/src/utils/fn.ts
Normal file
13
packages/core/src/utils/fn.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ZodSchema, z } from "zod";
|
||||
|
||||
export function fn<
|
||||
Arg1 extends ZodSchema,
|
||||
Callback extends (arg1: z.output<Arg1>) => any,
|
||||
>(arg1: Arg1, cb: Callback) {
|
||||
const result = function (input: z.input<typeof arg1>): ReturnType<Callback> {
|
||||
const parsed = arg1.parse(input);
|
||||
return cb.apply(cb, [parsed as any]);
|
||||
};
|
||||
result.schema = arg1;
|
||||
return result;
|
||||
}
|
||||
1
packages/core/src/utils/index.ts
Normal file
1
packages/core/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./fn"
|
||||
42
packages/core/sst-env.d.ts
vendored
Normal file
42
packages/core/sst-env.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
import "sst"
|
||||
export {}
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"Auth": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"AuthFingerprintKey": {
|
||||
"type": "random.index/randomString.RandomString"
|
||||
"value": string
|
||||
}
|
||||
"CloudflareAuthKV": {
|
||||
"type": "sst.cloudflare.Kv"
|
||||
}
|
||||
"InstantAdminToken": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"InstantAppId": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"LoopsApiKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Urls": {
|
||||
"api": string
|
||||
"auth": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/core/tsconfig.json
Normal file
9
packages/core/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user