mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +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:
81
packages/functions/src/party/auth.ts
Normal file
81
packages/functions/src/party/auth.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { z } from "zod";
|
||||
import { Hono } from "hono";
|
||||
import { Result } from "../common"
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import type * as Party from "partykit/server";
|
||||
import { validator, resolver } from "hono-openapi/zod";
|
||||
|
||||
const paramsObj = z.object({
|
||||
code: z.string(),
|
||||
state: z.string()
|
||||
})
|
||||
|
||||
export module AuthApi {
|
||||
export const route = new Hono()
|
||||
.get("/:connection",
|
||||
describeRoute({
|
||||
tags: ["Auth"],
|
||||
summary: "Authenticate the remote device",
|
||||
description: "This is a callback function to authenticate the remote device.",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(z.literal("Device authenticated successfully"))
|
||||
},
|
||||
},
|
||||
description: "Authentication successful.",
|
||||
},
|
||||
404: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.object({ error: z.string() })),
|
||||
},
|
||||
},
|
||||
description: "This device does not exist.",
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
connection: z.string().openapi({
|
||||
description: "The hostname of the device to login to.",
|
||||
example: "desktopeuo8vsf",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const param = c.req.valid("param");
|
||||
const env = c.env as any
|
||||
const room = env.room as Party.Room
|
||||
|
||||
const connection = room.getConnection(param.connection)
|
||||
if (!connection) {
|
||||
return c.json({ error: "This device does not exist." }, 404);
|
||||
}
|
||||
|
||||
const authParams = getUrlParams(new URL(c.req.url))
|
||||
const res = paramsObj.safeParse(authParams)
|
||||
if (res.error) {
|
||||
return c.json({ error: "Expected url params are missing" })
|
||||
}
|
||||
|
||||
connection.send(JSON.stringify({ ...authParams, type: "auth" }))
|
||||
|
||||
// FIXME:We just assume the authentication was successful, might wanna do some questioning in the future
|
||||
return c.text("Device authenticated successfully")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function getUrlParams(url: URL) {
|
||||
const urlString = url.toString()
|
||||
const hash = urlString.substring(urlString.indexOf('?') + 1); // Extract the part after the #
|
||||
const params = new URLSearchParams(hash);
|
||||
const paramsObj = {} as any;
|
||||
for (const [key, value] of params.entries()) {
|
||||
paramsObj[key] = decodeURIComponent(value);
|
||||
}
|
||||
return paramsObj;
|
||||
}
|
||||
116
packages/functions/src/party/hono.ts
Normal file
116
packages/functions/src/party/hono.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import "zod-openapi/extend";
|
||||
import type * as Party from "partykit/server";
|
||||
// import { Resource } from "sst";
|
||||
import { ZodError } from "zod";
|
||||
import { logger } from "hono/logger";
|
||||
// import { subjects } from "../subjects";
|
||||
import { VisibleError } from "../error";
|
||||
// import { ActorContext } from '@nestri/core/actor';
|
||||
import { Hono, type MiddlewareHandler } from "hono";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { AuthApi } from "./auth";
|
||||
|
||||
|
||||
const app = new Hono().basePath('/parties/main/:id');
|
||||
// const auth: MiddlewareHandler = async (c, next) => {
|
||||
// const client = createClient({
|
||||
// clientID: "api",
|
||||
// issuer: "http://auth.nestri.io" //Resource.Urls.auth
|
||||
// });
|
||||
|
||||
// const authHeader =
|
||||
// c.req.query("authorization") ?? c.req.header("authorization");
|
||||
// if (authHeader) {
|
||||
// const match = authHeader.match(/^Bearer (.+)$/);
|
||||
// if (!match || !match[1]) {
|
||||
// throw new VisibleError(
|
||||
// "input",
|
||||
// "auth.token",
|
||||
// "Bearer token not found or improperly formatted",
|
||||
// );
|
||||
// }
|
||||
// const bearerToken = match[1];
|
||||
|
||||
// const result = await client.verify(subjects, bearerToken!);
|
||||
// if (result.err)
|
||||
// throw new VisibleError("input", "auth.invalid", "Invalid bearer token");
|
||||
// if (result.subject.type === "user") {
|
||||
// // return ActorContext.with(
|
||||
// // {
|
||||
// // type: "user",
|
||||
// // properties: {
|
||||
// // accessToken: result.subject.properties.accessToken,
|
||||
// // userID: result.subject.properties.userID,
|
||||
// // auth: {
|
||||
// // type: "oauth",
|
||||
// // clientID: result.aud,
|
||||
// // },
|
||||
// // },
|
||||
// // },
|
||||
// // next,
|
||||
// // );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
app
|
||||
.use(logger(), async (c, next) => {
|
||||
c.header("Cache-Control", "no-store");
|
||||
return next();
|
||||
})
|
||||
// .use(auth)
|
||||
|
||||
|
||||
app
|
||||
.route("/auth", AuthApi.route)
|
||||
// .get("/parties/main/:id", (c) => {
|
||||
// const id = c.req.param();
|
||||
// const env = c.env as any
|
||||
// const party = env.room as Party.Room
|
||||
// party.broadcast("hello from hono")
|
||||
|
||||
// return c.text(`Hello there, ${id.id} 👋🏾`)
|
||||
// })
|
||||
.onError((error, c) => {
|
||||
console.error(error);
|
||||
if (error instanceof VisibleError) {
|
||||
return c.json(
|
||||
{
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
error.kind === "auth" ? 401 : 400,
|
||||
);
|
||||
}
|
||||
if (error instanceof ZodError) {
|
||||
const e = error.errors[0];
|
||||
if (e) {
|
||||
return c.json(
|
||||
{
|
||||
code: e?.code,
|
||||
message: e?.message,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (error instanceof HTTPException) {
|
||||
return c.json(
|
||||
{
|
||||
code: "request",
|
||||
message: "Invalid request",
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
return c.json(
|
||||
{
|
||||
code: "internal",
|
||||
message: "Internal server error",
|
||||
},
|
||||
500,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
export default app
|
||||
53
packages/functions/src/party/index.ts
Normal file
53
packages/functions/src/party/index.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type * as Party from "partykit/server";
|
||||
import app from "./hono"
|
||||
export default class Server implements Party.Server {
|
||||
constructor(readonly room: Party.Room) { }
|
||||
|
||||
onRequest(request: Party.Request): Response | Promise<Response> {
|
||||
|
||||
return app.fetch(request as any, { room: this.room })
|
||||
}
|
||||
|
||||
getConnectionTags(
|
||||
conn: Party.Connection,
|
||||
ctx: Party.ConnectionContext
|
||||
) {
|
||||
console.log("Tagging", conn.id)
|
||||
// const country = (ctx.request.cf?.country as string) ?? "unknown";
|
||||
// return [country];
|
||||
return [conn.id]
|
||||
// return ["AF"]
|
||||
}
|
||||
|
||||
onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
|
||||
// A websocket just connected!
|
||||
this.getConnectionTags(conn, ctx)
|
||||
|
||||
console.log(
|
||||
`Connected:
|
||||
id: ${conn.id}
|
||||
room: ${this.room.id}
|
||||
url: ${new URL(ctx.request.url).pathname}`
|
||||
);
|
||||
|
||||
// let's send a message to the connection
|
||||
// conn.send("hello from server");
|
||||
}
|
||||
|
||||
onMessage(message: string, sender: Party.Connection) {
|
||||
// let's log the message
|
||||
console.log(`connection ${sender.id} sent message: ${message}`);
|
||||
// console.log("tags", this.room.getConnections())
|
||||
// for (const british of this.room.getConnections(sender.id)) {
|
||||
// british.send(`Pip-pip!`);
|
||||
// }
|
||||
// // as well as broadcast it to all the other connections in the room...
|
||||
// this.room.broadcast(
|
||||
// `${sender.id}: ${message}`,
|
||||
// // ...except for the connection it came from
|
||||
// [sender.id]
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
Server satisfies Party.Worker;
|
||||
Reference in New Issue
Block a user