mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-16 18:55:37 +02:00
⭐ feat(infra): Update infra and add support for teams to SST (#186)
## Description - [x] Adds support for AWS SSO, which makes us (the team) able to use SST and update the components independently - [x] Splits the webpage into the landing page (Qwik), and Astro (the console) in charge of playing. This allows us to pass in Environment Variables to the console - ~Migrates the docs from Nuxt to Nextjs, and connects them to SST. This allows us to use Fumadocs _citation needed_ that's much more beautiful, and supports OpenApi~ - Cloudflare pages with github integration is not working on our new CF account. So we will have to push the pages deployment manually with Github actions - [x] Moves the current set up from my personal CF and AWS accounts to dedicated Nestri accounts - ## Related Issues <!-- List any related issues (e.g., "Closes #123", "Fixes #456") --> ## Type of Change - [ ] Bug fix (non-breaking change) - [x] New feature (non-breaking change) - [ ] Breaking change (fix or feature that changes existing functionality) - [x] Documentation update - [ ] Other (please describe): ## Checklist - [x] I have updated relevant documentation - [x] My code follows the project's coding style - [x] My changes generate no new warnings/errors ## Notes for Reviewers <!-- Point out areas you'd like reviewers to focus on, questions you have, or decisions that need discussion --> Please approve my PR 🥹 ## Screenshots/Demo <!-- If applicable, add screenshots or a GIF demo of your changes (especially for UI changes) --> ## Additional Context <!-- Add any other context about the pull request here -->
This commit is contained in:
@@ -1,40 +1,17 @@
|
||||
import { Resource } from "sst"
|
||||
import {
|
||||
type ExecutionContext,
|
||||
type KVNamespace,
|
||||
} from "@cloudflare/workers-types"
|
||||
import { Select } from "./ui/select";
|
||||
import { subjects } from "./subjects"
|
||||
import { logger } from "hono/logger";
|
||||
import { handle } from "hono/aws-lambda";
|
||||
import { PasswordUI } from "./ui/password"
|
||||
import { Email } from "@nestri/core/email/index"
|
||||
import { Users } from "@nestri/core/user/index"
|
||||
import { Teams } from "@nestri/core/team/index"
|
||||
import { authorizer } from "@openauthjs/openauth"
|
||||
import { Profiles } from "@nestri/core/profile/index"
|
||||
import { issuer } from "@openauthjs/openauth";
|
||||
import { User } from "@nestri/core/user/index"
|
||||
import { Email } from "@nestri/core/email/index";
|
||||
import { handleDiscord, handleGithub } from "./utils";
|
||||
import { type CFRequest } from "@nestri/core/types"
|
||||
import { GithubAdapter } from "./ui/adapters/github";
|
||||
import { DiscordAdapter } from "./ui/adapters/discord";
|
||||
import { Instances } from "@nestri/core/instance/index"
|
||||
import { PasswordAdapter } from "./ui/adapters/password"
|
||||
import { type Adapter } from "@openauthjs/openauth/adapter/adapter"
|
||||
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
|
||||
import { Subscriptions } from "@nestri/core/subscription/index";
|
||||
import type { Subscription } from "./type";
|
||||
interface Env {
|
||||
CloudflareAuthKV: KVNamespace
|
||||
}
|
||||
|
||||
export type CodeAdapterState =
|
||||
| {
|
||||
type: "start"
|
||||
}
|
||||
| {
|
||||
type: "code"
|
||||
resend?: boolean
|
||||
code: string
|
||||
claims: Record<string, string>
|
||||
}
|
||||
import { type Provider } from "@openauthjs/openauth/provider/provider"
|
||||
|
||||
type OauthUser = {
|
||||
primary: {
|
||||
@@ -45,156 +22,176 @@ type OauthUser = {
|
||||
avatar: any;
|
||||
username: any;
|
||||
}
|
||||
export default {
|
||||
async fetch(request: CFRequest, env: Env, ctx: ExecutionContext) {
|
||||
// const location = `${request.cf.country},${request.cf.continent}`
|
||||
return authorizer({
|
||||
select: Select({
|
||||
providers: {
|
||||
device: {
|
||||
hide: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
theme: {
|
||||
title: "Nestri | Auth",
|
||||
primary: "#FF4F01",
|
||||
//TODO: Change this in prod
|
||||
logo: "https://nestri.io/logo.webp",
|
||||
favicon: "https://nestri.io/seo/favicon.ico",
|
||||
background: {
|
||||
light: "#f5f5f5 ",
|
||||
dark: "#171717"
|
||||
},
|
||||
radius: "lg",
|
||||
font: {
|
||||
family: "Geist, sans-serif",
|
||||
},
|
||||
css: `
|
||||
const app = issuer({
|
||||
select: Select({
|
||||
providers: {
|
||||
device: {
|
||||
hide: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
theme: {
|
||||
title: "Nestri | Auth",
|
||||
primary: "#FF4F01",
|
||||
//TODO: Change this in prod
|
||||
logo: "https://nestri.io/logo.webp",
|
||||
favicon: "https://nestri.io/seo/favicon.ico",
|
||||
background: {
|
||||
light: "#f5f5f5 ",
|
||||
dark: "#171717"
|
||||
},
|
||||
radius: "lg",
|
||||
font: {
|
||||
family: "Geist, sans-serif",
|
||||
},
|
||||
css: `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
||||
`,
|
||||
},
|
||||
storage: CloudflareStorage({
|
||||
namespace: env.CloudflareAuthKV,
|
||||
},
|
||||
subjects,
|
||||
providers: {
|
||||
github: GithubAdapter({
|
||||
clientID: Resource.GithubClientID.value,
|
||||
clientSecret: Resource.GithubClientSecret.value,
|
||||
scopes: ["user:email"]
|
||||
}),
|
||||
discord: DiscordAdapter({
|
||||
clientID: Resource.DiscordClientID.value,
|
||||
clientSecret: Resource.DiscordClientSecret.value,
|
||||
scopes: ["email", "identify"]
|
||||
}),
|
||||
password: PasswordAdapter(
|
||||
PasswordUI({
|
||||
sendCode: async (email, code) => {
|
||||
console.log("email & code:", email, code)
|
||||
await Email.send(
|
||||
"auth",
|
||||
email,
|
||||
`Nestri code: ${code}`,
|
||||
`Your Nestri login code is ${code}`,
|
||||
)
|
||||
},
|
||||
}),
|
||||
subjects,
|
||||
providers: {
|
||||
github: GithubAdapter({
|
||||
clientID: Resource.GithubClientID.value,
|
||||
clientSecret: Resource.GithubClientSecret.value,
|
||||
scopes: ["user:email"]
|
||||
}),
|
||||
discord: DiscordAdapter({
|
||||
clientID: Resource.DiscordClientID.value,
|
||||
clientSecret: Resource.DiscordClientSecret.value,
|
||||
scopes: ["email", "identify"]
|
||||
}),
|
||||
password: PasswordAdapter(
|
||||
PasswordUI({
|
||||
sendCode: async (email, code) => {
|
||||
console.log("email & code:", email, code)
|
||||
await Email.send(email, code)
|
||||
},
|
||||
}),
|
||||
),
|
||||
device: {
|
||||
type: "device",
|
||||
async client(input) {
|
||||
if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
|
||||
throw new Error("Invalid authorization token");
|
||||
}
|
||||
const teamSlug = input.params.team;
|
||||
if (!teamSlug) {
|
||||
throw new Error("Team slug is required");
|
||||
}
|
||||
|
||||
const hostname = input.params.hostname;
|
||||
if (!hostname) {
|
||||
throw new Error("Hostname is required");
|
||||
}
|
||||
|
||||
return {
|
||||
hostname,
|
||||
teamSlug
|
||||
};
|
||||
},
|
||||
init() { }
|
||||
} as Adapter<{ teamSlug: string; hostname: string; }>,
|
||||
},
|
||||
allow: async (input) => {
|
||||
const url = new URL(input.redirectURI);
|
||||
const hostname = url.hostname;
|
||||
if (hostname.endsWith("nestri.io")) return true;
|
||||
if (hostname === "localhost") return true;
|
||||
return false;
|
||||
},
|
||||
success: async (ctx, value) => {
|
||||
if (value.provider === "device") {
|
||||
const team = await Teams.fromSlug(value.teamSlug)
|
||||
console.log("team", team)
|
||||
console.log("teamSlug", value.teamSlug)
|
||||
if (team) {
|
||||
await Instances.create({ hostname: value.hostname, teamID: team.id })
|
||||
|
||||
return await ctx.subject("device", {
|
||||
teamSlug: value.teamSlug,
|
||||
hostname: value.hostname,
|
||||
})
|
||||
}
|
||||
),
|
||||
device: {
|
||||
type: "device",
|
||||
async client(input) {
|
||||
if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
|
||||
throw new Error("Invalid authorization token");
|
||||
}
|
||||
const teamSlug = input.params.team;
|
||||
if (!teamSlug) {
|
||||
throw new Error("Team slug is required");
|
||||
}
|
||||
|
||||
if (value.provider === "password") {
|
||||
const email = value.email
|
||||
const username = value.username
|
||||
const token = await Users.create(email)
|
||||
const usr = await Users.fromEmail(email);
|
||||
const exists = await Profiles.fromOwnerID(usr.id)
|
||||
if (username && !exists) {
|
||||
await Profiles.create({ owner: usr.id, username })
|
||||
}
|
||||
const hostname = input.params.hostname;
|
||||
if (!hostname) {
|
||||
throw new Error("Hostname is required");
|
||||
}
|
||||
|
||||
return await ctx.subject("user", {
|
||||
accessToken: token,
|
||||
userID: usr.id,
|
||||
return {
|
||||
hostname,
|
||||
teamSlug
|
||||
};
|
||||
},
|
||||
init() { }
|
||||
} as Provider<{ teamSlug: string; hostname: string; }>,
|
||||
},
|
||||
allow: async (input) => {
|
||||
const url = new URL(input.redirectURI);
|
||||
const hostname = url.hostname;
|
||||
if (hostname.endsWith("nestri.io")) return true;
|
||||
if (hostname === "localhost") return true;
|
||||
return false;
|
||||
},
|
||||
success: async (ctx, value) => {
|
||||
// if (value.provider === "device") {
|
||||
// const team = await Teams.fromSlug(value.teamSlug)
|
||||
// console.log("team", team)
|
||||
// console.log("teamSlug", value.teamSlug)
|
||||
// if (team) {
|
||||
// await Instances.create({ hostname: value.hostname, teamID: team.id })
|
||||
|
||||
// return await ctx.subject("device", {
|
||||
// teamSlug: value.teamSlug,
|
||||
// hostname: value.hostname,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
if (value.provider === "password") {
|
||||
const email = value.email
|
||||
const username = value.username
|
||||
const matching = await User.fromEmail(email)
|
||||
|
||||
//Sign Up
|
||||
if (username && !matching) {
|
||||
const userID = await User.create({
|
||||
name: username,
|
||||
email,
|
||||
});
|
||||
|
||||
if (!userID) throw new Error("Error creating user");
|
||||
|
||||
return ctx.subject("user", {
|
||||
userID,
|
||||
email
|
||||
});
|
||||
} else if (matching) {
|
||||
//Sign In
|
||||
return ctx.subject("user", {
|
||||
userID: matching.id,
|
||||
email
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let user = undefined as OauthUser | undefined;
|
||||
|
||||
if (value.provider === "github") {
|
||||
const access = value.tokenset.access;
|
||||
user = await handleGithub(access)
|
||||
}
|
||||
|
||||
if (value.provider === "discord") {
|
||||
const access = value.tokenset.access
|
||||
user = await handleDiscord(access)
|
||||
}
|
||||
|
||||
if (user) {
|
||||
try {
|
||||
const matching = await User.fromEmail(user.primary.email);
|
||||
|
||||
//Sign Up
|
||||
if (!matching) {
|
||||
const userID = await User.create({
|
||||
email: user.primary.email,
|
||||
name: user.username,
|
||||
avatarUrl: user.avatar
|
||||
});
|
||||
|
||||
if (!userID) throw new Error("Error creating user");
|
||||
|
||||
return ctx.subject("user", {
|
||||
userID,
|
||||
email: user.primary.email
|
||||
});
|
||||
} else {
|
||||
//Sign In
|
||||
return await ctx.subject("user", {
|
||||
userID: matching.id,
|
||||
email: user.primary.email
|
||||
});
|
||||
}
|
||||
|
||||
let user = undefined as OauthUser | undefined;
|
||||
} catch (error) {
|
||||
console.error("error registering the user", error)
|
||||
}
|
||||
|
||||
if (value.provider === "github") {
|
||||
const access = value.tokenset.access;
|
||||
user = await handleGithub(access)
|
||||
}
|
||||
}
|
||||
|
||||
if (value.provider === "discord") {
|
||||
const access = value.tokenset.access
|
||||
user = await handleDiscord(access)
|
||||
}
|
||||
throw new Error("Something went seriously wrong");
|
||||
},
|
||||
}).use(logger())
|
||||
|
||||
if (user) {
|
||||
try {
|
||||
const token = await Users.create(user.primary.email)
|
||||
const usr = await Users.fromEmail(user.primary.email);
|
||||
const exists = await Profiles.fromOwnerID(usr.id)
|
||||
console.log("exists", exists)
|
||||
if (!exists) {
|
||||
await Profiles.create({ owner: usr.id, avatarUrl: user.avatar, username: user.username })
|
||||
}
|
||||
|
||||
return await ctx.subject("user", {
|
||||
accessToken: token,
|
||||
userID: usr.id,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("error registering the user", error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new Error("Something went seriously wrong");
|
||||
},
|
||||
}).fetch(request, env, ctx)
|
||||
}
|
||||
}
|
||||
export const handler = handle(app)
|
||||
|
||||
Reference in New Issue
Block a user