├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── package.json ├── packages ├── astro │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ └── launch.json │ ├── README.md │ ├── astro.config.mjs │ ├── package.json │ ├── public │ │ └── favicon.svg │ ├── src │ │ ├── components │ │ │ └── Card.astro │ │ ├── env.d.ts │ │ ├── layouts │ │ │ └── Layout.astro │ │ └── pages │ │ │ ├── auth │ │ │ └── callback.ts │ │ │ └── index.astro │ ├── sst-env.d.ts │ └── tsconfig.json ├── core │ ├── package.json │ ├── src │ │ ├── bus.ts │ │ ├── dynamo.ts │ │ ├── spotify.ts │ │ ├── time.ts │ │ └── user.ts │ ├── sst-env.d.ts │ └── tsconfig.json └── functions │ ├── package.json │ ├── src │ ├── authenticator.ts │ ├── events │ │ └── user.ts │ └── lambda.ts │ ├── sst-env.d.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── sst.config.ts ├── stacks └── MyStack.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | .build 7 | 8 | # misc 9 | .DS_Store 10 | 11 | # local env files 12 | .env*.local -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug SST Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", 9 | "runtimeArgs": ["start", "--increase-timeout"], 10 | "console": "integratedTerminal", 11 | "skipFiles": ["/**"], 12 | "env": {} 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.sst": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqr", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "sso": "aws sso login --profile=ironbay-dev", 8 | "dev": "sst dev", 9 | "build": "sst build", 10 | "deploy": "sst deploy", 11 | "remove": "sst remove", 12 | "console": "sst console", 13 | "typecheck": "tsc --noEmit" 14 | }, 15 | "devDependencies": { 16 | "sst": "2.1.17", 17 | "aws-cdk-lib": "2.62.2", 18 | "constructs": "10.1.156", 19 | "typescript": "^4.9.5", 20 | "@tsconfig/node16": "^1.0.3" 21 | }, 22 | "workspaces": [ 23 | "packages/*" 24 | ] 25 | } -------------------------------------------------------------------------------- /packages/astro/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /packages/astro/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /packages/astro/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/astro/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Basics 2 | 3 | ``` 4 | npm create astro@latest -- --template basics 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ![basics](https://user-images.githubusercontent.com/4677417/186188965-73453154-fdec-4d6b-9c34-cb35c248ae5b.png) 14 | 15 | 16 | ## 🚀 Project Structure 17 | 18 | Inside of your Astro project, you'll see the following folders and files: 19 | 20 | ``` 21 | / 22 | ├── public/ 23 | │ └── favicon.svg 24 | ├── src/ 25 | │ ├── components/ 26 | │ │ └── Card.astro 27 | │ ├── layouts/ 28 | │ │ └── Layout.astro 29 | │ └── pages/ 30 | │ └── index.astro 31 | └── package.json 32 | ``` 33 | 34 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 35 | 36 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 37 | 38 | Any static assets, like images, can be placed in the `public/` directory. 39 | 40 | ## 🧞 Commands 41 | 42 | All commands are run from the root of the project, from a terminal: 43 | 44 | | Command | Action | 45 | | :--------------------- | :----------------------------------------------- | 46 | | `npm install` | Installs dependencies | 47 | | `npm run dev` | Starts local dev server at `localhost:3000` | 48 | | `npm run build` | Build your production site to `./dist/` | 49 | | `npm run preview` | Preview your build locally, before deploying | 50 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 51 | | `npm run astro --help` | Get help using the Astro CLI | 52 | 53 | ## 👀 Want to learn more? 54 | 55 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 56 | -------------------------------------------------------------------------------- /packages/astro/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import aws from "astro-sst/lambda"; 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | output: "server", 7 | adapter: aws(), 8 | vite: { optimizeDeps: { exclude: ["sst"] } }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/astro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "sst bind astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "astro": "2.1.0", 14 | "sst": "2.1.17" 15 | }, 16 | "devDependencies": { 17 | "astro-sst": "^2.1.17" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/astro/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /packages/astro/src/components/Card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | title: string; 4 | body: string; 5 | href: string; 6 | } 7 | 8 | const { href, title, body } = Astro.props; 9 | --- 10 | 11 | 22 | 64 | -------------------------------------------------------------------------------- /packages/astro/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/astro/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {title} 17 | 18 | 19 | 20 | 21 | 22 | 36 | -------------------------------------------------------------------------------- /packages/astro/src/pages/auth/callback.ts: -------------------------------------------------------------------------------- 1 | import type { APIRoute } from "astro"; 2 | import { Auth } from "sst/node/future/auth"; 3 | 4 | export const get: APIRoute = async (ctx) => { 5 | const code = ctx.url.searchParams.get("code"); 6 | if (!code) { 7 | throw new Error("Code missing"); 8 | } 9 | const response = await fetch(Auth.auth.url + "/token", { 10 | method: "POST", 11 | body: new URLSearchParams({ 12 | grant_type: "authorization_code", 13 | client_id: "local", 14 | code, 15 | redirect_uri: `${ctx.url.origin}${ctx.url.pathname}`, 16 | }), 17 | }).then((r) => r.json()); 18 | ctx.cookies.set("sst_auth_token", response.access_token, { 19 | maxAge: 60 * 60 * 24 * 30, 20 | path: "/", 21 | }); 22 | return ctx.redirect("/", 302); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/astro/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import Card from '../components/Card.astro'; 4 | import { Auth, useSession } from "sst/node/future/auth" 5 | 6 | const params = new URLSearchParams({ 7 | client_id: "local", 8 | redirect_uri: `${Astro.url.origin}/auth/callback`, 9 | response_type: "code", 10 | provider: "spotify", 11 | }) 12 | 13 | const url = Auth.auth.url + "/authorize?" + params.toString() 14 | console.log("asd", Astro.url.searchParams.toString()) 15 | --- 16 | 17 | 18 |
19 |

Welcome to Astro

20 |

21 | To get started, open the directory src/pages in your project.
22 | Code Challenge: Tweak the "Welcome to Astro" message above. 23 |

24 | 46 |
47 |
48 | 49 | 93 | -------------------------------------------------------------------------------- /packages/astro/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/astro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest" 3 | } -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sqr/core", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "test": "sst bind 'vitest'", 7 | "typecheck": "tsc -noEmit" 8 | }, 9 | "devDependencies": { 10 | "@aws-sdk/types": "^3.0.0", 11 | "@types/node": "^18.13.0", 12 | "sst": "2.1.17", 13 | "vitest": "^0.28.4" 14 | }, 15 | "dependencies": { 16 | "@aws-sdk/client-dynamodb": "^3.266.1", 17 | "@aws-sdk/client-eventbridge": "^3.267.0", 18 | "@aws-sdk/smithy-client": "^3.0.0", 19 | "aws-sdk": "^2.1310.0", 20 | "electrodb": "^2.4.1", 21 | "spotify-api.js": "^9.2.5", 22 | "sst": "2.1.17" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/core/src/bus.ts: -------------------------------------------------------------------------------- 1 | export * as Bus from "./bus"; 2 | import { EventBus } from "sst/node/event-bus"; 3 | import { 4 | EventBridgeClient, 5 | PutEventsCommand, 6 | } from "@aws-sdk/client-eventbridge"; 7 | import { Handler } from "sst/context"; 8 | 9 | export interface Events { 10 | test: { 11 | foo: string; 12 | }; 13 | } 14 | 15 | type EventTypes = keyof Events; 16 | 17 | const client = new EventBridgeClient({}); 18 | 19 | export async function publish( 20 | type: Type, 21 | properties: Events[Type] 22 | ) { 23 | console.log("Publishing event", type); 24 | await client.send( 25 | new PutEventsCommand({ 26 | Entries: [ 27 | { 28 | EventBusName: EventBus.bus.eventBusName, 29 | Source: "sqr", 30 | Detail: JSON.stringify({ 31 | type, 32 | properties, 33 | }), 34 | DetailType: type, 35 | }, 36 | ], 37 | }) 38 | ); 39 | } 40 | 41 | export function subscribe( 42 | _type: Type, 43 | handler: (properties: Events[Type]) => Promise 44 | ) { 45 | return Handler("sqs", async (evt) => { 46 | const failed: string[] = []; 47 | for (const record of evt.Records) { 48 | const { detail } = JSON.parse(record.body); 49 | try { 50 | await handler(detail.properties); 51 | } catch (err) { 52 | console.error(err); 53 | failed.push(record.messageId); 54 | } 55 | } 56 | 57 | return { 58 | batchItemFailures: failed.map((f) => ({ 59 | itemIdentifier: f, 60 | })), 61 | }; 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /packages/core/src/dynamo.ts: -------------------------------------------------------------------------------- 1 | import DynamoDB from "aws-sdk/clients/dynamodb"; 2 | import { Table } from "sst/node/table"; 3 | export * as Dynamo from "./dynamo"; 4 | 5 | export const Client = new DynamoDB.DocumentClient(); 6 | export const Service = { 7 | client: Client, 8 | table: Table.table.tableName, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/core/src/spotify.ts: -------------------------------------------------------------------------------- 1 | export * as Spotify from "./spotify"; 2 | 3 | import Spotify from "spotify-api.js"; 4 | import { Config } from "sst/node/config"; 5 | 6 | export interface Credentials { 7 | access: string; 8 | refresh: string; 9 | } 10 | 11 | export function client(input: Credentials) { 12 | return Spotify.Client.create({ 13 | token: { 14 | token: input.access, 15 | refreshToken: input.refresh, 16 | clientID: Config.SPOTIFY_CLIENT_ID, 17 | clientSecret: Config.SPOTIFY_CLIENT_SECRET, 18 | }, 19 | userAuthorizedToken: true, 20 | }); 21 | } 22 | 23 | export async function sync(client: Spotify.Client) { 24 | const playlists = Object.fromEntries( 25 | await client.user 26 | .getPlaylists({}, true) 27 | .then((r) => r.map((p) => [p.name, p])) 28 | ); 29 | await Promise.all( 30 | Object.values(playlists).map(async (p) => { 31 | playlists[p.name].tracks = await client.playlists.getTracks(p.id); 32 | }) 33 | ); 34 | const pending = new Map>(); 35 | for await (const track of tracks(client)) { 36 | const added = new Date(track.addedAt); 37 | const quarter = Math.floor(added.getUTCMonth() / 3) + 1; 38 | const playlist = `${added.getUTCFullYear()} Q${quarter}`; 39 | let existing = playlists[playlist]; 40 | if (!existing) { 41 | console.log("Creating playlist", playlist); 42 | const result = await client.user.createPlaylist({ 43 | name: playlist, 44 | description: "Created by Spotify Quarterly Report", 45 | public: false, 46 | }); 47 | playlists[playlist] = existing = result!; 48 | } 49 | if (existing.tracks?.some((t) => t.track?.id === track.item.id)) continue; 50 | let batch = pending.get(existing.id); 51 | if (!batch) pending.set(existing.id, (batch = new Set())); 52 | batch.add(track.item.uri); 53 | } 54 | 55 | for (const [id, batch] of pending) { 56 | console.log("Adding", batch.size, "tracks to playlist", id); 57 | await client.playlists.addItems(id, [...batch]); 58 | } 59 | } 60 | 61 | export async function* tracks(client: Spotify.Client) { 62 | console.log("fetching tracks"); 63 | let offset = 0; 64 | while (true) { 65 | const tracks = await client.user.getSavedTracks({ 66 | offset, 67 | }); 68 | for (const track of tracks) { 69 | yield track; 70 | } 71 | console.log("fetched", tracks.length, "tracks from offset", offset); 72 | if (tracks.length === 0) break; 73 | offset += tracks.length; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/core/src/time.ts: -------------------------------------------------------------------------------- 1 | export * as Time from "./time"; 2 | 3 | export function now() { 4 | return new Date().toISOString(); 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/user.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "electrodb"; 2 | import { Dynamo } from "./dynamo"; 3 | export * as User from "./user"; 4 | 5 | const UserEntity = new Entity( 6 | { 7 | model: { 8 | entity: "user", 9 | version: "1", 10 | service: "sqr", 11 | }, 12 | attributes: { 13 | userID: { 14 | type: "string", 15 | required: true, 16 | }, 17 | spotifyID: { 18 | type: "string", 19 | required: true, 20 | }, 21 | refreshToken: { 22 | type: "string", 23 | required: true, 24 | }, 25 | accessToken: { 26 | type: "string", 27 | required: true, 28 | }, 29 | }, 30 | indexes: { 31 | primary: { 32 | pk: { 33 | field: "pk", 34 | composite: ["userID"], 35 | }, 36 | sk: { 37 | field: "sk", 38 | composite: [], 39 | }, 40 | }, 41 | bySpotifyID: { 42 | index: "gsi1", 43 | pk: { 44 | field: "gsi1pk", 45 | composite: ["spotifyID"], 46 | }, 47 | sk: { 48 | field: "gsi1sk", 49 | composite: [], 50 | }, 51 | }, 52 | }, 53 | }, 54 | Dynamo.Service 55 | ); 56 | 57 | import crypto from "crypto"; 58 | import { Bus } from "./bus"; 59 | import { Spotify } from "./spotify"; 60 | 61 | declare module "./bus" { 62 | export interface Events { 63 | "user.login": { 64 | userID: string; 65 | }; 66 | } 67 | } 68 | 69 | export async function fromID(userID: string) { 70 | const result = await UserEntity.get({ 71 | userID, 72 | }).go(); 73 | return result.data; 74 | } 75 | 76 | export async function login(input: Spotify.Credentials) { 77 | const client = await Spotify.client(input); 78 | 79 | const existing = await UserEntity.query 80 | .bySpotifyID({ 81 | spotifyID: client.user.id, 82 | }) 83 | .go(); 84 | if (!existing.data[0]) { 85 | const user = await UserEntity.create({ 86 | userID: crypto.randomUUID(), 87 | spotifyID: client.user.id, 88 | refreshToken: input.refresh, 89 | accessToken: input.access, 90 | }).go(); 91 | await Bus.publish("user.login", { 92 | userID: user.data.userID, 93 | }); 94 | return user.data; 95 | } 96 | 97 | const result = await UserEntity.update({ 98 | userID: existing.data[0].userID, 99 | }) 100 | .set({ 101 | refreshToken: input.refresh, 102 | accessToken: input.access, 103 | }) 104 | .go({ 105 | response: "all_new", 106 | }); 107 | await Bus.publish("user.login", { 108 | userID: result.data!.userID!, 109 | }); 110 | 111 | return result.data!; 112 | } 113 | -------------------------------------------------------------------------------- /packages/core/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sqr/functions", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "test": "sst bind 'vitest'", 7 | "typecheck": "tsc -noEmit" 8 | }, 9 | "devDependencies": { 10 | "@types/aws-lambda": "^8.10.110", 11 | "@types/node": "^18.13.0", 12 | "sst": "2.1.17" 13 | }, 14 | "dependencies": { 15 | "openid-client": "^5.4.0", 16 | "sst": "2.1.17" 17 | } 18 | } -------------------------------------------------------------------------------- /packages/functions/src/authenticator.ts: -------------------------------------------------------------------------------- 1 | import { AuthHandler, OauthAdapter } from "sst/node/future/auth"; 2 | import { Config } from "sst/node/config"; 3 | import { Issuer } from "openid-client"; 4 | import { User } from "@sqr/core/user"; 5 | 6 | declare module "sst/node/future/auth" { 7 | interface SessionTypes { 8 | user: { 9 | userID: string; 10 | }; 11 | } 12 | } 13 | 14 | export const handler = AuthHandler({ 15 | clients: async () => { 16 | return { 17 | local: "http://localhost", 18 | }; 19 | }, 20 | providers: { 21 | spotify: OauthAdapter({ 22 | clientID: Config.SPOTIFY_CLIENT_ID, 23 | clientSecret: Config.SPOTIFY_CLIENT_SECRET, 24 | scope: 25 | "playlist-read-private user-read-email user-library-read playlist-modify-private", 26 | issuer: new Issuer({ 27 | issuer: "https://accounts.spotify.com", 28 | authorization_endpoint: "https://accounts.spotify.com/authorize", 29 | token_endpoint: "https://accounts.spotify.com/api/token", 30 | }), 31 | }), 32 | }, 33 | onSuccess: async (result) => { 34 | if (result.provider === "spotify") { 35 | const user = await User.login({ 36 | access: result.tokenset.access_token!, 37 | refresh: result.tokenset.refresh_token!, 38 | }); 39 | 40 | return { 41 | type: "user", 42 | properties: { 43 | userID: user.userID!, 44 | }, 45 | }; 46 | } 47 | 48 | return { 49 | type: "public", 50 | properties: {}, 51 | }; 52 | }, 53 | onError: async () => { 54 | return { 55 | statusCode: 500, 56 | body: "something went wrong", 57 | }; 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /packages/functions/src/events/user.ts: -------------------------------------------------------------------------------- 1 | import { Bus } from "@sqr/core/bus"; 2 | import { Spotify } from "@sqr/core/spotify"; 3 | import { User } from "@sqr/core/user"; 4 | 5 | export const login = Bus.subscribe("user.login", async (evt) => { 6 | const user = await User.fromID(evt.userID); 7 | if (!user) throw new Error(`User ${evt.userID} does not exist`); 8 | const client = await Spotify.client({ 9 | access: user.accessToken, 10 | refresh: user.refreshToken, 11 | }); 12 | 13 | await Spotify.sync(client); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/functions/src/lambda.ts: -------------------------------------------------------------------------------- 1 | import { ApiHandler } from "sst/node/api"; 2 | import { Time } from "@sqr/core/time"; 3 | 4 | export const handler = ApiHandler(async (_evt) => { 5 | return { 6 | body: `Hello world. The time is ${Time.now()}`, 7 | }; 8 | }); 9 | -------------------------------------------------------------------------------- /packages/functions/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@sqr/core/*": ["../core/src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/**/*" 3 | -------------------------------------------------------------------------------- /sst.config.ts: -------------------------------------------------------------------------------- 1 | import { SSTConfig } from "sst"; 2 | import { API } from "./stacks/MyStack"; 3 | import { Api } from "sst/constructs"; 4 | 5 | export default { 6 | config(input) { 7 | const profile: Record = { 8 | dev: "ironbay-dev", 9 | production: "ironbay-production", 10 | }; 11 | return { 12 | name: "sqr", 13 | region: "us-east-1", 14 | profile: profile[input.stage || ""] || profile.dev, 15 | }; 16 | }, 17 | stacks(app) { 18 | app.stack(API); 19 | }, 20 | } satisfies SSTConfig; 21 | -------------------------------------------------------------------------------- /stacks/MyStack.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StackContext, 3 | Api, 4 | Table, 5 | EventBus, 6 | Queue, 7 | toCdkDuration, 8 | } from "sst/constructs"; 9 | import { Auth } from "sst/constructs/future"; 10 | import { Config } from "sst/constructs"; 11 | 12 | export function API({ stack }: StackContext) { 13 | const spotify = Config.Secret.create( 14 | stack, 15 | "SPOTIFY_CLIENT_SECRET", 16 | "SPOTIFY_CLIENT_ID" 17 | ); 18 | 19 | const table = new Table(stack, "table", { 20 | fields: { 21 | pk: "string", 22 | sk: "string", 23 | gsi1pk: "string", 24 | gsi1sk: "string", 25 | gsi2pk: "string", 26 | gsi2sk: "string", 27 | }, 28 | primaryIndex: { 29 | partitionKey: "pk", 30 | sortKey: "sk", 31 | }, 32 | globalIndexes: { 33 | gsi1: { 34 | partitionKey: "gsi1pk", 35 | sortKey: "gsi1sk", 36 | }, 37 | gsi2: { 38 | partitionKey: "gsi2pk", 39 | sortKey: "gsi2sk", 40 | }, 41 | }, 42 | }); 43 | 44 | const bus = new EventBus(stack, "bus"); 45 | 46 | bus.addRules(stack, { 47 | "user.login": { 48 | pattern: { 49 | detailType: ["user.login"], 50 | }, 51 | targets: { 52 | queue: new Queue(stack, "user-created-queue", { 53 | cdk: { 54 | queue: { 55 | visibilityTimeout: toCdkDuration("15 minutes"), 56 | }, 57 | }, 58 | consumer: { 59 | function: { 60 | handler: "packages/functions/src/events/user.login", 61 | timeout: "15 minutes", 62 | bind: [ 63 | table, 64 | spotify.SPOTIFY_CLIENT_SECRET, 65 | spotify.SPOTIFY_CLIENT_ID, 66 | ], 67 | }, 68 | }, 69 | }), 70 | }, 71 | }, 72 | }); 73 | 74 | const auth = new Auth(stack, "auth", { 75 | authenticator: { 76 | handler: "packages/functions/src/authenticator.handler", 77 | bind: [ 78 | table, 79 | bus, 80 | spotify.SPOTIFY_CLIENT_ID, 81 | spotify.SPOTIFY_CLIENT_SECRET, 82 | ], 83 | }, 84 | }); 85 | 86 | const api = new Api(stack, "api", { 87 | defaults: { 88 | function: { 89 | bind: [bus, table, ...Object.values(spotify)], 90 | }, 91 | }, 92 | routes: { 93 | "GET /": "packages/functions/src/lambda.handler", 94 | }, 95 | }); 96 | 97 | stack.addOutputs({ 98 | ApiEndpoint: api.url, 99 | Bus: bus.eventBusArn, 100 | AuthUrl: auth.url, 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------