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:
87
apps/docs/RELAY.md
Normal file
87
apps/docs/RELAY.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# How to Deploy Your Own MoQ Relay on a Server
|
||||
|
||||
This guide will walk you through the steps to deploy your own MoQ relay on a server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Server Requirements:**
|
||||
- Ensure port 443 is open for both TCP and UDP (`:443/udp & :443/tcp`).
|
||||
- The server should have a minimum of **4GB RAM** and **2 vCPUs**.
|
||||
- Supports ARM or AMD64 architecture.
|
||||
|
||||
2. **Software Requirements:**
|
||||
- Docker and `docker-compose` must be installed on the server. You can use [this installation script](https://github.com/docker/docker-install) for Docker.
|
||||
- Git must be installed to clone the necessary repository.
|
||||
|
||||
3. **Certificates:**
|
||||
- You will need private and public certificates. It is recommended to use certificates from a trusted CA rather than self-signed certificates.
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### Step 1: Clone the Repository
|
||||
|
||||
Clone the `kixelated/moq-rs` repository to your local machine:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kixelated/moq-rs moq
|
||||
```
|
||||
|
||||
### Step 2: Verify Port Availability
|
||||
|
||||
Check if port 443 is already in use on your server:
|
||||
|
||||
```bash
|
||||
sudo netstat -tulpn | grep ':443' | grep LISTEN
|
||||
```
|
||||
or
|
||||
```bash
|
||||
sudo lsof -i -P -n | grep LISTEN | grep 443
|
||||
```
|
||||
|
||||
If you find any processes using port 443, consider terminating them.
|
||||
|
||||
### Step 3: Configure Ports
|
||||
|
||||
Navigate to the cloned directory and edit the Docker compose file to use port 443:
|
||||
|
||||
```bash
|
||||
cd moq
|
||||
vim docker-compose.yml
|
||||
```
|
||||
|
||||
Change the ports section from lines 34 to 35 to:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
```
|
||||
|
||||
### Step 4: Prepare Certificates
|
||||
|
||||
Copy your generated certificates into the `moq/dev` directory and rename them:
|
||||
|
||||
```bash
|
||||
cp cert.pem moq/dev/localhost.crt
|
||||
cp key.pem moq/dev/localhost.key
|
||||
```
|
||||
|
||||
### Step 5: Start Docker Instances
|
||||
|
||||
Ensure you are in the root directory of the `moq` project, then start the Docker containers:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Step 6: Link Domain to Server IP
|
||||
|
||||
Configure your DNS settings to connect your server's IP address to your domain:
|
||||
|
||||
```
|
||||
Record Type: A
|
||||
Subdomain: relay.fst.so
|
||||
IP Address: xx.xxx.xx.xxx
|
||||
```
|
||||
|
||||
Congratulations, your MoQ server is now set up! You can verify its functionality by using the [MoQ Checker](https://nestri.pages.dev/moq/checker).
|
||||
42
apps/docs/sst-env.d.ts
vendored
Normal file
42
apps/docs/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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"@nestri/eslint-config/qwik.js",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:qwik/recommended",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
@@ -12,5 +19,24 @@ module.exports = {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"prefer-spread": "off",
|
||||
"no-case-declarations": "off",
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error"],
|
||||
"@typescript-eslint/consistent-type-imports": "warn",
|
||||
"@typescript-eslint/no-unnecessary-condition": "warn",
|
||||
},
|
||||
};
|
||||
@@ -35,11 +35,10 @@
|
||||
"@builder.io/qwik-city": "^1.8.0",
|
||||
"@builder.io/qwik-react": "0.5.0",
|
||||
"@modular-forms/qwik": "^0.27.0",
|
||||
"@nestri/eslint-config": "*",
|
||||
"@nestri/input": "*",
|
||||
"@nestri/libmoq": "*",
|
||||
"@nestri/typescript-config": "*",
|
||||
"@nestri/ui": "*",
|
||||
"@openauthjs/openauth": "^0.2.6",
|
||||
"@types/eslint": "8.56.10",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/node": "^22.5.1",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// module.exports = require("@nestri/ui/postcss.config");
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
|
||||
9
apps/www/src/routes/(auth)/device/index.tsx
Normal file
9
apps/www/src/routes/(auth)/device/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { component$ } from "@builder.io/qwik"
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<div class="font-title">
|
||||
Device
|
||||
</div>
|
||||
)
|
||||
})
|
||||
54
apps/www/src/routes/(auth)/login/index.tsx
Normal file
54
apps/www/src/routes/(auth)/login/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { $, component$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
|
||||
function getHashParams(url: URL) {
|
||||
const urlString = url.toString()
|
||||
const hash = urlString.substring(urlString.indexOf('#') + 1); // Extract the part after the #
|
||||
console.log("url", hash)
|
||||
const params = new URLSearchParams(hash);
|
||||
const paramsObj = {} as any;
|
||||
for (const [key, value] of params.entries()) {
|
||||
paramsObj[key] = decodeURIComponent(value);
|
||||
}
|
||||
console.log(paramsObj)
|
||||
return paramsObj;
|
||||
}
|
||||
|
||||
function removeURLParams() {
|
||||
const newURL = window.location.origin + window.location.pathname; // Just origin and path
|
||||
window.location.replace(newURL);
|
||||
}
|
||||
|
||||
export default component$(() => {
|
||||
|
||||
const login = $(async () => {
|
||||
const client = createClient({
|
||||
clientID: "www",
|
||||
issuer: "https://auth.lauryn.dev.nestri.io"
|
||||
})
|
||||
|
||||
const { url } = await client.authorize("http://localhost:5173/login", "token", { pkce: true })
|
||||
window.location.href = url
|
||||
})
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(async () => {
|
||||
const urlObj = new URL(window.location.href);
|
||||
const params = getHashParams(urlObj)
|
||||
if (params.access_token && params.refresh_token) {
|
||||
|
||||
localStorage.setItem("access_token", params.access_token)
|
||||
localStorage.setItem("refresh_token", params.refresh_token)
|
||||
removeURLParams()
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
return (
|
||||
<div class="h-screen w-screen flex justify-center items-center">
|
||||
<button class="px-2 py-1 font-title text-lg bg-gray-400 rounded-lg" onClick$={login}>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,119 +0,0 @@
|
||||
import * as v from "valibot"
|
||||
//FIXME: Make sure this works
|
||||
// import { Broadcast } from "./tester";
|
||||
import { cn } from "@nestri/ui/design";
|
||||
import { routeLoader$ } from "@builder.io/qwik-city";
|
||||
import { component$, $, useSignal } from "@builder.io/qwik";
|
||||
import { MotionComponent, transition, TitleSection, Button } from "@nestri/ui/react";
|
||||
import { type InitialValues, type SubmitHandler, useForm, valiForm$ } from "@modular-forms/qwik"
|
||||
|
||||
const Schema = v.object({
|
||||
url: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(10, "Please input a valid url"),
|
||||
v.url("Please input a valid url"),
|
||||
)
|
||||
}, "Please fill in all the fields correctly.")
|
||||
|
||||
type Form = v.InferInput<typeof Schema>;
|
||||
|
||||
export const useFormLoader = routeLoader$<InitialValues<Form>>(async () => {
|
||||
return {
|
||||
url: ""
|
||||
}
|
||||
})
|
||||
|
||||
const generateRandomWord = (length: number) => {
|
||||
const characters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
return Array.from({ length }, () => characters[Math.floor(Math.random() * characters.length)]).join('');
|
||||
};
|
||||
|
||||
export default component$(() => {
|
||||
const broadcasterOk = useSignal<boolean | undefined>();
|
||||
const [state, { Form, Field }] = useForm<Form>({
|
||||
loader: useFormLoader(),
|
||||
validate: valiForm$(Schema)
|
||||
});
|
||||
|
||||
const handleSubmit = $<SubmitHandler<Form>>(async (values) => {
|
||||
const randomNamespace = generateRandomWord(6);
|
||||
// const sub = await Broadcast.init({ url: values.url, fingerprint: undefined, namespace: randomNamespace })
|
||||
|
||||
// setTimeout(() => {
|
||||
// broadcasterOk.value = sub.isSubscribed()
|
||||
// }, 1000);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection client:load title="MoQ Checker" description="Test the connection to your Media-Over-Quic relay!" />
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="flex items-center justify-center w-full"
|
||||
as="div"
|
||||
>
|
||||
<section class="w-full flex flex-col gap-4 justify-center items-center">
|
||||
<Form onSubmit$={handleSubmit} class="w-full max-w-xl flex px-3 gap-2">
|
||||
<Field name="url">
|
||||
{(field, props) => {
|
||||
return (
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
<div class="bg-gray-200 dark:bg-gray-800 flex rounded-lg w-full relative h-10 flex-none border focus-within:bg-gray-300/70 dark:focus-within:bg-gray-700/70 border-gray-300 dark:border-gray-700 ">
|
||||
<input type="url" class={cn("w-full relative h-full bg-transparent rounded-lg p-3 focus-within:outline-none focus-within:ring-2 focus-within:ring-gray-400 dark:focus-within:ring-gray-600 focus-within:ring-offset-2 focus-visible:outline-none focus-within:ring-offset-gray-100 dark:focus-within:ring-offset-gray-900 placeholder:text-gray-500/70", typeof broadcasterOk.value !== "undefined" && broadcasterOk.value == true && "ring-2 ring-offset-2 ring-offset-gray-100 dark:ring-offset-gray-900 ring-green-500", typeof broadcasterOk.value !== "undefined" && (broadcasterOk.value == false) && "ring-2 ring-offset-2 ring-offset-gray-100 dark:ring-offset-gray-900 ring-red-500")} placeholder="https://relay.domain.com" {...props} />
|
||||
</div>
|
||||
{field.error && (<p class='text-[0.8rem] font-medium text-danger-600 dark:text-danger-500' >{field.error}</p>)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Field>
|
||||
|
||||
{/* <button class={cn(buttonVariants.solid({ size: "md", intent: "neutral" }), "w-max space-y-0 relative")} style={{ height: 40, marginTop: 0 }} type="submit" >
|
||||
Check
|
||||
</button> */}
|
||||
<Button.Root
|
||||
disabled={state.submitting}
|
||||
isLoading={state.submitting}
|
||||
// setIsLoading={setIsLoading}
|
||||
client:load
|
||||
//@ts-ignore
|
||||
type="submit"
|
||||
style={{ height: 40, marginTop: 0 }}
|
||||
intent="neutral"
|
||||
size="md"
|
||||
class="w-max space-y-0 relative">
|
||||
{/* <Button.Icon
|
||||
isLoading={isLoading.value}
|
||||
client:load>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16">
|
||||
<g fill="currentColor">
|
||||
<path d="M.329 10.333A8.01 8.01 0 0 0 7.99 16C12.414 16 16 12.418 16 8s-3.586-8-8.009-8A8.006 8.006 0 0 0 0 7.468l.003.006l4.304 1.769A2.2 2.2 0 0 1 5.62 8.88l1.96-2.844l-.001-.04a3.046 3.046 0 0 1 3.042-3.043a3.046 3.046 0 0 1 3.042 3.043a3.047 3.047 0 0 1-3.111 3.044l-2.804 2a2.223 2.223 0 0 1-3.075 2.11a2.22 2.22 0 0 1-1.312-1.568L.33 10.333Z" /><path d="M4.868 12.683a1.715 1.715 0 0 0 1.318-3.165a1.7 1.7 0 0 0-1.263-.02l1.023.424a1.261 1.261 0 1 1-.97 2.33l-.99-.41a1.7 1.7 0 0 0 .882.84Zm3.726-6.687a2.03 2.03 0 0 0 2.027 2.029a2.03 2.03 0 0 0 2.027-2.029a2.03 2.03 0 0 0-2.027-2.027a2.03 2.03 0 0 0-2.027 2.027m2.03-1.527a1.524 1.524 0 1 1-.002 3.048a1.524 1.524 0 0 1 .002-3.048" />
|
||||
</g>
|
||||
</svg>
|
||||
</Button.Icon> */}
|
||||
<Button.Label
|
||||
loadingText="Checking..."
|
||||
class="text-ellipsis whitespace-nowrap"
|
||||
isLoading={state.submitting}>
|
||||
Check
|
||||
</Button.Label>
|
||||
<div class="w-[8%]" />
|
||||
</Button.Root>
|
||||
</Form>
|
||||
{typeof broadcasterOk.value !== "undefined" && broadcasterOk.value == true ? (
|
||||
<span class="w-full text-green-500 max-w-xl flex space-y-6 px-3 gap-2">
|
||||
Your relay is doing okay
|
||||
</span>
|
||||
) : typeof broadcasterOk.value !== "undefined" && (
|
||||
<span class="w-full text-red-500 max-w-xl flex space-y-6 px-3 gap-2">
|
||||
Your relay has an issue
|
||||
</span>
|
||||
)}
|
||||
</section>
|
||||
</MotionComponent>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -1,208 +0,0 @@
|
||||
// import type { Connection, SubscribeRecv } from "@nestri/libmoq/transport"
|
||||
// import { asError } from "@nestri/moq/common/error"
|
||||
// import { Client } from "@nestri/moq/transport/client"
|
||||
// import * as Catalog from "@nestri/moq/media/catalog"
|
||||
// import { type GroupWriter } from "@nestri/moq/transport/objects"
|
||||
|
||||
// export interface BroadcastConfig {
|
||||
// namespace: string
|
||||
// connection: Connection
|
||||
// }
|
||||
// export interface BroadcasterConfig {
|
||||
// url: string
|
||||
// namespace: string
|
||||
// fingerprint?: string // URL to fetch TLS certificate fingerprint
|
||||
// }
|
||||
|
||||
// export interface BroadcastConfigTrack {
|
||||
// input: string
|
||||
// bitrate: number
|
||||
// }
|
||||
|
||||
// export class Broadcast {
|
||||
// stream: GroupWriter | null
|
||||
// subscriber: SubscribeRecv | null
|
||||
// subscribed: boolean;
|
||||
|
||||
|
||||
// readonly config: BroadcastConfig
|
||||
// readonly catalog: Catalog.Root
|
||||
// readonly connection: Connection
|
||||
// readonly namespace: string
|
||||
|
||||
// #running: Promise<void>
|
||||
|
||||
// constructor(config: BroadcastConfig) {
|
||||
// this.subscribed = false
|
||||
// this.namespace = config.namespace
|
||||
// this.connection = config.connection
|
||||
// this.config = config
|
||||
// //Arbitrary values, just to keep TypeScript happy :)
|
||||
// this.catalog = {
|
||||
// version: 1,
|
||||
// streamingFormat: 1,
|
||||
// streamingFormatVersion: "0.2",
|
||||
// supportsDeltaUpdates: false,
|
||||
// commonTrackFields: {
|
||||
// packaging: "loc",
|
||||
// renderGroup: 1,
|
||||
// },
|
||||
// tracks: [{
|
||||
// name: "tester",
|
||||
// namespace: "tester",
|
||||
// selectionParams: {}
|
||||
// }],
|
||||
// }
|
||||
// this.stream = null
|
||||
// this.subscriber = null
|
||||
|
||||
// this.#running = this.#run()
|
||||
// }
|
||||
|
||||
// static async init(config: BroadcasterConfig): Promise<Broadcast> {
|
||||
// const client = new Client({ url: config.url, fingerprint: config.fingerprint, role: "publisher" })
|
||||
// const connection = await client.connect();
|
||||
|
||||
// return new Broadcast({ connection, namespace: config.namespace })
|
||||
// }
|
||||
|
||||
// async #run() {
|
||||
// try {
|
||||
// await this.connection.announce(this.namespace)
|
||||
// this.subscribed = true
|
||||
// } catch (error) {
|
||||
|
||||
// this.subscribed = false
|
||||
// }
|
||||
|
||||
// for (; ;) {
|
||||
// const subscriber = await this.connection.subscribed()
|
||||
|
||||
// if (!subscriber) {
|
||||
// this.subscribed = false
|
||||
|
||||
// break
|
||||
// }
|
||||
|
||||
// await subscriber.ack()
|
||||
|
||||
// this.subscriber = subscriber
|
||||
|
||||
// this.subscribed = true
|
||||
|
||||
// const bytes = Catalog.encode(this.catalog);
|
||||
|
||||
// const stream = await subscriber.group({ group: 0 });
|
||||
|
||||
// await stream.write({ object: 0, payload: bytes })
|
||||
|
||||
// this.stream = stream
|
||||
// }
|
||||
// }
|
||||
|
||||
// isSubscribed(): boolean {
|
||||
// return this.subscribed;
|
||||
// }
|
||||
|
||||
// // async #serveSubscribe(subscriber: SubscribeRecv) {
|
||||
// // try {
|
||||
|
||||
// // // Send a SUBSCRIBE_OK
|
||||
// // await subscriber.ack()
|
||||
|
||||
// // console.log("catalog track name:", subscriber.track)
|
||||
|
||||
// // const stream = await subscriber.group({ group: 0 });
|
||||
|
||||
// // // const bytes = this.catalog.encode("Hello World")
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes })
|
||||
|
||||
|
||||
|
||||
// // } catch (e) {
|
||||
// // const err = asError(e)
|
||||
// // await subscriber.close(1n, `failed to process publish: ${err.message}`)
|
||||
// // } finally {
|
||||
// // // TODO we can't close subscribers because there's no support for clean termination
|
||||
// // // await subscriber.close()
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // async mouseUpdatePosition({ x, y }: { x: number, y: number }, stream: GroupWriter) {
|
||||
|
||||
// // const mouse_move = {
|
||||
// // input_type: "mouse_move",
|
||||
// // delta_y: y,
|
||||
// // delta_x: x,
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// // async mouseUpdateButtons(e: MouseEvent, stream: GroupWriter) {
|
||||
// // const data: { input_type?: "mouse_key_down" | "mouse_key_up"; button: number; } = { button: e.button };
|
||||
|
||||
// // if (e.type === "mousedown") {
|
||||
// // data["input_type"] = "mouse_key_down"
|
||||
// // } else if (e.type === "mouseup") {
|
||||
// // data["input_type"] = "mouse_key_up"
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// // async mouseUpdateWheel(e: WheelEvent, stream: GroupWriter) {
|
||||
// // const data: { input_type?: "mouse_wheel_up" | "mouse_wheel_down" } = {}
|
||||
|
||||
// // if (e.deltaY < 0.0) {
|
||||
// // data["input_type"] = "mouse_wheel_up"
|
||||
// // } else {
|
||||
// // data["input_type"] = "mouse_wheel_down"
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// // async updateKeyUp(e: KeyboardEvent, stream: GroupWriter) {
|
||||
// // const data = {
|
||||
// // input_type: "key_up",
|
||||
// // key_code: e.keyCode
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// // async updateKeyDown(e: KeyboardEvent, stream: GroupWriter) {
|
||||
// // const data = {
|
||||
// // input_type: "key_down",
|
||||
// // key_code: e.keyCode
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// close() {
|
||||
// // TODO implement publish close
|
||||
// }
|
||||
|
||||
// // Returns the error message when the connection is closed
|
||||
// async closed(): Promise<Error> {
|
||||
// try {
|
||||
// await this.#running
|
||||
// return new Error("closed") // clean termination
|
||||
// } catch (e) {
|
||||
// return asError(e)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,12 +1,5 @@
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { HomeNavBar, Card } from "@nestri/ui";
|
||||
|
||||
function getGreeting(): string {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 5 && hour < 12) return "Good Morning";
|
||||
if (hour >= 12 && hour < 18) return "Good Afternoon";
|
||||
return "Good Evening";
|
||||
}
|
||||
import { HomeNavBar } from "@nestri/ui";
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {useLocation} from "@builder.io/qwik-city";
|
||||
import {Keyboard, Mouse, WebRTCStream} from "@nestri/input"
|
||||
import {component$, useSignal, useVisibleTask$} from "@builder.io/qwik";
|
||||
import { useLocation } from "@builder.io/qwik-city";
|
||||
import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"
|
||||
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
||||
|
||||
export default component$(() => {
|
||||
const id = useLocation().params.id;
|
||||
const canvas = useSignal<HTMLCanvasElement>();
|
||||
|
||||
useVisibleTask$(({track}) => {
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(({ track }) => {
|
||||
track(() => canvas.value);
|
||||
|
||||
if (!canvas.value) return; // Ensure canvas is available
|
||||
@@ -66,9 +67,9 @@ export default component$(() => {
|
||||
// @ts-ignore
|
||||
if (document.pointerLockElement && !window.nestrimouse && !window.nestrikeyboard) {
|
||||
// @ts-ignore
|
||||
window.nestrimouse = new Mouse({canvas: canvas.value, webrtc});
|
||||
window.nestrimouse = new Mouse({ canvas: canvas.value, webrtc });
|
||||
// @ts-ignore
|
||||
window.nestrikeyboard = new Keyboard({canvas: canvas.value, webrtc});
|
||||
window.nestrikeyboard = new Keyboard({ canvas: canvas.value, webrtc });
|
||||
// @ts-ignore
|
||||
} else if (!document.pointerLockElement && window.nestrimouse && window.nestrikeyboard) {
|
||||
// @ts-ignore
|
||||
@@ -180,7 +181,7 @@ export default component$(() => {
|
||||
}
|
||||
}}
|
||||
//TODO: go full screen, then lock on "landscape" screen-orientation on mobile
|
||||
class="aspect-video h-full w-full object-contain max-h-screen"/>
|
||||
class="aspect-video h-full w-full object-contain max-h-screen" />
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { $, component$, noSerialize, type NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { TitleSection, MotionComponent, transition } from "@nestri/ui/react";
|
||||
import { NavBar, Footer, Book } from "@nestri/ui"
|
||||
import { cn } from "@nestri/ui/design";
|
||||
import { Howl, Howler } from 'howler';
|
||||
import { Howl } from 'howler';
|
||||
|
||||
//FIXME: Add a FAQ section
|
||||
// FIXME: Takes too long for the price input radio input to become responsive
|
||||
@@ -68,6 +68,7 @@ export default component$(() => {
|
||||
const bookRef = useSignal<HTMLButtonElement | undefined>()
|
||||
const audio = useSignal<NoSerialize<Howl> | undefined>()
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(() => {
|
||||
audio.value = noSerialize(new Howl({ src: ["/audio/cash.mp3"], volume: 0.5 }))
|
||||
|
||||
|
||||
40
apps/www/sst-env.d.ts
vendored
40
apps/www/sst-env.d.ts
vendored
@@ -1,4 +1,42 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
/* 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
{
|
||||
"extends": "@nestri/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"target": "ES2022",
|
||||
"target": "ES2017",
|
||||
"module": "ES2022",
|
||||
"lib": [
|
||||
"es2022",
|
||||
"DOM",
|
||||
"WebWorker",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@builder.io/qwik",
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"incremental": true,
|
||||
@@ -24,22 +17,9 @@
|
||||
"outDir": "tmp",
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"content-collections": [
|
||||
"./.content-collections/generated"
|
||||
]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
".eslintrc.cjs"
|
||||
],
|
||||
"include": [
|
||||
"src",
|
||||
"./*.d.ts",
|
||||
"./*.config.ts",
|
||||
"./*.config.js",
|
||||
"content-collections.ts"
|
||||
, "../../packages/input/src/webrtc-stream.ts" ]
|
||||
"files": ["./.eslintrc.cjs"],
|
||||
"include": ["src", "./*.d.ts", "./*.config.ts","./*.config.cjs"]
|
||||
}
|
||||
Reference in New Issue
Block a user