Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
887 changes: 601 additions & 286 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions apps/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ shuttle-posthog = { path = "../../crates/shuttle-posthog" }
shuttle-clerk = { path = "../../crates/shuttle-clerk" }
shuttle-stt = { path = "../../crates/shuttle-stt" }

tokio = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
futures = { workspace = true }

axum = { version = "0.7.4", features = ["ws"] }
axum = { version = "0.7.4", features = ["ws", "multipart"] }
tower-http = { version = "0.6.2", features = ["fs", "timeout"] }
reqwest = "0.12.9"
async-openai = { version = "0.26.0", default-features = false }
Expand All @@ -34,5 +34,4 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
schemars = { workspace = true, features = ["preserve_order"] }

lettre = "0.11.11"
async-stripe = { version = "0.39.1", features = ["runtime-tokio-hyper"] }
14 changes: 9 additions & 5 deletions apps/server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use tower_http::{
mod auth;
mod native;
mod state;
mod stripe;
mod web;

#[shuttle_runtime::main]
Expand Down Expand Up @@ -72,17 +73,20 @@ async fn main(
auth::middleware_fn,
));

let web_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../web");
let webhook_router = Router::new().route("/stripe", post(stripe::webhook::handler));

let router = Router::new()
.route("/health", get(health))
.nest("/api/native", native_router)
.nest("/api/web", web_router)
.route("/health", get(health))
.fallback_service(
.nest("/webhook", webhook_router)
.fallback_service({
let web_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../web");

ServeDir::new(web_dir.join("dist"))
.append_index_html_on_directories(false)
.fallback(ServeFile::new(web_dir.join("dist/index.html"))),
)
.fallback(ServeFile::new(web_dir.join("dist/index.html")))
})
.with_state(state);

Ok(router.into())
Expand Down
37 changes: 37 additions & 0 deletions apps/server/src/stripe/extractor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// https://github.com/arlyon/async-stripe/blob/c71a7eb6d7115089193f7dd602a0ce356848ab73/examples/webhook-axum.rs

use axum::{
async_trait,
body::Body,
extract::FromRequest,
http::{Request, StatusCode},
response::{IntoResponse, Response},
};

pub struct StripeEvent(pub stripe::Event);

#[async_trait]
impl<S> FromRequest<S> for StripeEvent
where
String: FromRequest<S>,
S: Send + Sync,
{
type Rejection = Response;

async fn from_request(req: Request<Body>, state: &S) -> Result<Self, Self::Rejection> {
let signature = if let Some(sig) = req.headers().get("stripe-signature") {
sig.to_owned()
} else {
return Err(StatusCode::BAD_REQUEST.into_response());
};

let payload = String::from_request(req, state)
.await
.map_err(IntoResponse::into_response)?;

Ok(Self(
stripe::Webhook::construct_event(&payload, signature.to_str().unwrap(), "whsec_xxxxx")
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?,
))
}
}
2 changes: 2 additions & 0 deletions apps/server/src/stripe/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod extractor;
pub mod webhook;
25 changes: 25 additions & 0 deletions apps/server/src/stripe/webhook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use super::extractor::StripeEvent;
use axum::response::IntoResponse;
use stripe::{EventObject, EventType};

pub async fn handler(StripeEvent(event): StripeEvent) -> impl IntoResponse {
match event.type_ {
EventType::CheckoutSessionCompleted => {
if let EventObject::CheckoutSession(session) = event.data.object {
println!(
"Received checkout session completed webhook with id: {:?}",
session.id
);
}
}
EventType::AccountUpdated => {
if let EventObject::Account(account) = event.data.object {
println!(
"Received account updated webhook for account: {:?}",
account.id
);
}
}
_ => println!("Unknown event encountered in webhook: {:?}", event.type_),
}
}
1 change: 0 additions & 1 deletion apps/web/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
Expand Down
107 changes: 80 additions & 27 deletions apps/web/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,38 @@
// Import Routes

import { Route as rootRoute } from "./routes/__root";
import { Route as SettingsImport } from "./routes/settings";
import { Route as IndexImport } from "./routes/index";
import { Route as NotesIdImport } from "./routes/notes.$id";
import { Route as AuthSignUpImport } from "./routes/auth.sign-up";
import { Route as AuthSignOutImport } from "./routes/auth.sign-out";
import { Route as AuthSignInImport } from "./routes/auth.sign-in";
import { Route as AuthConnectImport } from "./routes/auth.connect";
import { Route as AuthConnectSuccessImport } from "./routes/auth.connect.success";
import { Route as AuthConnectFailedImport } from "./routes/auth.connect.failed";

// Create/Update Routes

const SettingsRoute = SettingsImport.update({
id: "/settings",
path: "/settings",
getParentRoute: () => rootRoute,
} as any);

const IndexRoute = IndexImport.update({
id: "/",
path: "/",
getParentRoute: () => rootRoute,
} as any);

const AuthSignUpRoute = AuthSignUpImport.update({
id: "/auth/sign-up",
path: "/auth/sign-up",
const NotesIdRoute = NotesIdImport.update({
id: "/notes/$id",
path: "/notes/$id",
getParentRoute: () => rootRoute,
} as any);

const AuthSignOutRoute = AuthSignOutImport.update({
id: "/auth/sign-out",
path: "/auth/sign-out",
const AuthSignUpRoute = AuthSignUpImport.update({
id: "/auth/sign-up",
path: "/auth/sign-up",
getParentRoute: () => rootRoute,
} as any);

Expand All @@ -56,6 +64,12 @@ const AuthConnectSuccessRoute = AuthConnectSuccessImport.update({
getParentRoute: () => AuthConnectRoute,
} as any);

const AuthConnectFailedRoute = AuthConnectFailedImport.update({
id: "/failed",
path: "/failed",
getParentRoute: () => AuthConnectRoute,
} as any);

// Populate the FileRoutesByPath interface

declare module "@tanstack/react-router" {
Expand All @@ -67,6 +81,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof IndexImport;
parentRoute: typeof rootRoute;
};
"/settings": {
id: "/settings";
path: "/settings";
fullPath: "/settings";
preLoaderRoute: typeof SettingsImport;
parentRoute: typeof rootRoute;
};
"/auth/connect": {
id: "/auth/connect";
path: "/auth/connect";
Expand All @@ -81,20 +102,27 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof AuthSignInImport;
parentRoute: typeof rootRoute;
};
"/auth/sign-out": {
id: "/auth/sign-out";
path: "/auth/sign-out";
fullPath: "/auth/sign-out";
preLoaderRoute: typeof AuthSignOutImport;
parentRoute: typeof rootRoute;
};
"/auth/sign-up": {
id: "/auth/sign-up";
path: "/auth/sign-up";
fullPath: "/auth/sign-up";
preLoaderRoute: typeof AuthSignUpImport;
parentRoute: typeof rootRoute;
};
"/notes/$id": {
id: "/notes/$id";
path: "/notes/$id";
fullPath: "/notes/$id";
preLoaderRoute: typeof NotesIdImport;
parentRoute: typeof rootRoute;
};
"/auth/connect/failed": {
id: "/auth/connect/failed";
path: "/failed";
fullPath: "/auth/connect/failed";
preLoaderRoute: typeof AuthConnectFailedImport;
parentRoute: typeof AuthConnectImport;
};
"/auth/connect/success": {
id: "/auth/connect/success";
path: "/success";
Expand All @@ -108,10 +136,12 @@ declare module "@tanstack/react-router" {
// Create and export the route tree

interface AuthConnectRouteChildren {
AuthConnectFailedRoute: typeof AuthConnectFailedRoute;
AuthConnectSuccessRoute: typeof AuthConnectSuccessRoute;
}

const AuthConnectRouteChildren: AuthConnectRouteChildren = {
AuthConnectFailedRoute: AuthConnectFailedRoute,
AuthConnectSuccessRoute: AuthConnectSuccessRoute,
};

Expand All @@ -121,74 +151,88 @@ const AuthConnectRouteWithChildren = AuthConnectRoute._addFileChildren(

export interface FileRoutesByFullPath {
"/": typeof IndexRoute;
"/settings": typeof SettingsRoute;
"/auth/connect": typeof AuthConnectRouteWithChildren;
"/auth/sign-in": typeof AuthSignInRoute;
"/auth/sign-out": typeof AuthSignOutRoute;
"/auth/sign-up": typeof AuthSignUpRoute;
"/notes/$id": typeof NotesIdRoute;
"/auth/connect/failed": typeof AuthConnectFailedRoute;
"/auth/connect/success": typeof AuthConnectSuccessRoute;
}

export interface FileRoutesByTo {
"/": typeof IndexRoute;
"/settings": typeof SettingsRoute;
"/auth/connect": typeof AuthConnectRouteWithChildren;
"/auth/sign-in": typeof AuthSignInRoute;
"/auth/sign-out": typeof AuthSignOutRoute;
"/auth/sign-up": typeof AuthSignUpRoute;
"/notes/$id": typeof NotesIdRoute;
"/auth/connect/failed": typeof AuthConnectFailedRoute;
"/auth/connect/success": typeof AuthConnectSuccessRoute;
}

export interface FileRoutesById {
__root__: typeof rootRoute;
"/": typeof IndexRoute;
"/settings": typeof SettingsRoute;
"/auth/connect": typeof AuthConnectRouteWithChildren;
"/auth/sign-in": typeof AuthSignInRoute;
"/auth/sign-out": typeof AuthSignOutRoute;
"/auth/sign-up": typeof AuthSignUpRoute;
"/notes/$id": typeof NotesIdRoute;
"/auth/connect/failed": typeof AuthConnectFailedRoute;
"/auth/connect/success": typeof AuthConnectSuccessRoute;
}

export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths:
| "/"
| "/settings"
| "/auth/connect"
| "/auth/sign-in"
| "/auth/sign-out"
| "/auth/sign-up"
| "/notes/$id"
| "/auth/connect/failed"
| "/auth/connect/success";
fileRoutesByTo: FileRoutesByTo;
to:
| "/"
| "/settings"
| "/auth/connect"
| "/auth/sign-in"
| "/auth/sign-out"
| "/auth/sign-up"
| "/notes/$id"
| "/auth/connect/failed"
| "/auth/connect/success";
id:
| "__root__"
| "/"
| "/settings"
| "/auth/connect"
| "/auth/sign-in"
| "/auth/sign-out"
| "/auth/sign-up"
| "/notes/$id"
| "/auth/connect/failed"
| "/auth/connect/success";
fileRoutesById: FileRoutesById;
}

export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
SettingsRoute: typeof SettingsRoute;
AuthConnectRoute: typeof AuthConnectRouteWithChildren;
AuthSignInRoute: typeof AuthSignInRoute;
AuthSignOutRoute: typeof AuthSignOutRoute;
AuthSignUpRoute: typeof AuthSignUpRoute;
NotesIdRoute: typeof NotesIdRoute;
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
SettingsRoute: SettingsRoute,
AuthConnectRoute: AuthConnectRouteWithChildren,
AuthSignInRoute: AuthSignInRoute,
AuthSignOutRoute: AuthSignOutRoute,
AuthSignUpRoute: AuthSignUpRoute,
NotesIdRoute: NotesIdRoute,
};

export const routeTree = rootRoute
Expand All @@ -202,30 +246,39 @@ export const routeTree = rootRoute
"filePath": "__root.tsx",
"children": [
"/",
"/settings",
"/auth/connect",
"/auth/sign-in",
"/auth/sign-out",
"/auth/sign-up"
"/auth/sign-up",
"/notes/$id"
]
},
"/": {
"filePath": "index.tsx"
},
"/settings": {
"filePath": "settings.tsx"
},
"/auth/connect": {
"filePath": "auth.connect.tsx",
"children": [
"/auth/connect/failed",
"/auth/connect/success"
]
},
"/auth/sign-in": {
"filePath": "auth.sign-in.tsx"
},
"/auth/sign-out": {
"filePath": "auth.sign-out.tsx"
},
"/auth/sign-up": {
"filePath": "auth.sign-up.tsx"
},
"/notes/$id": {
"filePath": "notes.$id.tsx"
},
"/auth/connect/failed": {
"filePath": "auth.connect.failed.tsx",
"parent": "/auth/connect"
},
"/auth/connect/success": {
"filePath": "auth.connect.success.tsx",
"parent": "/auth/connect"
Expand Down
Loading
Loading