diff --git a/apps/backend/README.md b/apps/backend/README.md index 9c9fc60..4d1a25e 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -4,3 +4,37 @@ ## Features - [x] Easy cross-function calls / Lambda triggers support with [hono-adapter-aws-lambda](https://github.com/NamesMT/hono-adapter-aws-lambda) + +## Structuring cookbook: +#### Root level: +Things like 3rd party APIs, DBs, Storages connectors, etc, should be placed in `~/providers` folder, grouped by their purpose if possible, e.g: `~/providers/auth/kinde-main.ts`, `~/providers/auth/google-main.ts`. + +Things that interact with `~/providers` should be placed in `~/services` folder. (like an `user` service) + +Other globally reuseable code should be placed in `~/helpers` folder. + +Locally reusable code should be placed in the same folder as the file that uses it, its name should be its usable scope, suffixing the file name with `.helper`, e.g: `/api/hello.helper.ts`, `/api/app.helper.ts`. + +#### `api` folder: +You could create folders to group/prefix the routes, e.g: `/api/auth` folder. + +The main app entry should be `app.ts`. + +Each route should be placed in a separate file according to the route path, e.g: `/api/hello.ts`, `/api/greet.ts`, +Alternatively, you could create a `routes.ts` for multiple routes declaration in file one, e.g: `/api/auth/routes.ts`. + +#### `import` order: +The import order is as following, and they should be separated with a line break: + +1. Package imports +2. Alias imports +3. Relative imports + +e.g: +```ts +import { env } from 'std-env' + +import { appFactory } from '~/factory' + +import { apiApp } from './api/app' +``` diff --git a/apps/backend/src/api/app.ts b/apps/backend/src/api/app.ts index d2c1ad4..7cc5d12 100644 --- a/apps/backend/src/api/app.ts +++ b/apps/backend/src/api/app.ts @@ -1,37 +1,26 @@ -// import type { TypedResponse } from 'hono' -// import { streamText } from 'hono/streaming' -import { type } from 'arktype' +import { appFactory } from '~/factory' import { authApp } from './auth/app' +import { greetRouteApp } from './greet' +import { helloRouteApp } from './hello' -import { appFactory } from '~/factory' -import { customArktypeValidator } from '~/helpers/arktype' - -const app = appFactory.createApp() - // $Auth - you'll need to setup Kinde environment variables. +export const apiApp = appFactory.createApp() + // Auth app - you'll need to setup Kinde environment variables. .route('/auth', authApp) -// Disabling the streaming API because https://github.com/sst/ion/issues/63 + // Simple health check route + .route('/hello', helloRouteApp) + + // Simple greet route for arktype input validation demo + .route('/greet', greetRouteApp) + +// ### This block contains the sample code for streaming APIs, +// import type { TypedResponse } from 'hono' +// import { streamText } from 'hono/streaming' + +// Do note that SST doesn't support Live Development for Lambda streaming API yet: https://github.com/sst/ion/issues/63 + // For RPC to know the type of streamed endpoints you could manually cast it with TypedResponse 👌 // .get('/helloStream', c => streamText(c, async (stream) => { // await stream.writeln('Hello from Hono `/api/helloStream`!') // }) as Response & TypedResponse<'Hello from Hono `/api/helloStream`!'>) - - // Simple health check route - .get('/hello', c => c.text(`Hello from Hono \`/api/hello\`! - ${Date.now()}`)) - - // Simple arktype input validation demo - .get( - '/hello/:name', - customArktypeValidator('param', type({ - name: 'string>0', - })), - async (c) => { - const { name } = c.req.valid('param') - return c.text(`Hello ${name}!`) - }, - ) - -export { - app as apiApp, -} diff --git a/apps/backend/src/api/auth/app.ts b/apps/backend/src/api/auth/app.ts index 2ec7c57..22dd83d 100644 --- a/apps/backend/src/api/auth/app.ts +++ b/apps/backend/src/api/auth/app.ts @@ -1,107 +1,6 @@ -import type { ClaimTokenType, FlagType } from '@kinde-oss/kinde-typescript-sdk' -import { env } from 'std-env' - -import { kindeClient } from './kindeClients' -import { getSessionManager } from './sessionManager' - import { appFactory } from '~/factory' -const app = appFactory.createApp() - .get('/health', async (c) => { - return c.text('Good', 200) - }) - - .get('/login', async (c) => { - const org_code = c.req.query('org_code') - const loginUrl = await kindeClient.login(getSessionManager(c), { org_code }) - - c.get('session').set('backToPath', c.req.query('path')) - - return c.redirect(loginUrl.toString()) - }) - - .get('/register', async (c) => { - const org_code = c.req.query('org_code') - const registerUrl = await kindeClient.register(getSessionManager(c), { org_code }) - return c.redirect(registerUrl.toString()) - }) - - .get('/callback', async (c) => { - await kindeClient.handleRedirectToApp(getSessionManager(c), new URL(c.req.url)) - - let backToPath = c.get('session').get('backToPath') as string || '/' - if (!backToPath.startsWith('/')) - backToPath = `/${backToPath}` - - return c.redirect(`${env.FRONTEND_URL!}${backToPath}`) - }) - - .get('/logout', async (c) => { - const logoutUrl = await kindeClient.logout(getSessionManager(c)) - return c.redirect(logoutUrl.toString()) - }) - - .get('/isAuth', async (c) => { - const isAuthenticated = await kindeClient.isAuthenticated(getSessionManager(c)) // Boolean: true or false - return c.json(isAuthenticated) - }) - - .get('/profile', async (c) => { - const profile = await kindeClient.getUserProfile(getSessionManager(c)) - return c.json(profile) - }) - - .get('/createOrg', async (c) => { - const org_name = c.req.query('org_name')?.toString() - const createUrl = await kindeClient.createOrg(getSessionManager(c), { org_name }) - return c.redirect(createUrl.toString()) - }) - - .get('/getOrg', async (c) => { - const org = await kindeClient.getOrganization(getSessionManager(c)) - return c.json(org) - }) - - .get('/getOrgs', async (c) => { - const orgs = await kindeClient.getUserOrganizations(getSessionManager(c)) - return c.json(orgs) - }) - - .get('/getPerm/:perm', async (c) => { - const perm = await kindeClient.getPermission(getSessionManager(c), c.req.param('perm')) - return c.json(perm) - }) - - .get('/getPerms', async (c) => { - const perms = await kindeClient.getPermissions(getSessionManager(c)) - return c.json(perms) - }) - - // Try: /api/auth/getClaim/aud, /api/auth/getClaim/email/id_token - .get('/getClaim/:claim', async (c) => { - const type = (c.req.query('type') ?? 'access_token') as ClaimTokenType - if (!/^(?:access_token|id_token)$/.test(type)) - return c.text('Bad request: type', 400) - - const claim = await kindeClient.getClaim(getSessionManager(c), c.req.param('claim'), type) - return c.json(claim) - }) - - .get('/getFlag/:code', async (c) => { - const claim = await kindeClient.getFlag( - getSessionManager(c), - c.req.param('code'), - c.req.query('default'), - c.req.query('flagType') as keyof FlagType | undefined, - ) - return c.json(claim) - }) - - .get('/getToken', async (c) => { - const accessToken = await kindeClient.getToken(getSessionManager(c)) - return c.text(accessToken) - }) +import { authRoutesApp } from './routes' -export { - app as authApp, -} +export const authApp = appFactory.createApp() + .route('', authRoutesApp) diff --git a/apps/backend/src/api/auth/routes.ts b/apps/backend/src/api/auth/routes.ts new file mode 100644 index 0000000..d0c7617 --- /dev/null +++ b/apps/backend/src/api/auth/routes.ts @@ -0,0 +1,102 @@ +import { env } from 'std-env' +import type { ClaimTokenType, FlagType } from '@kinde-oss/kinde-typescript-sdk' + +import { appFactory } from '~/factory' +import { getSessionManager } from '~/helpers/kinde' +import { kindeClient } from '~/providers/auth/kinde-main' + +export const authRoutesApp = appFactory.createApp() + .get('/health', async (c) => { + return c.text('Good', 200) + }) + + .get('/login', async (c) => { + const org_code = c.req.query('org_code') + const loginUrl = await kindeClient.login(getSessionManager(c), { org_code }) + + c.get('session').set('backToPath', c.req.query('path')) + + return c.redirect(loginUrl.toString()) + }) + + .get('/register', async (c) => { + const org_code = c.req.query('org_code') + const registerUrl = await kindeClient.register(getSessionManager(c), { org_code }) + return c.redirect(registerUrl.toString()) + }) + + .get('/callback', async (c) => { + await kindeClient.handleRedirectToApp(getSessionManager(c), new URL(c.req.url)) + + let backToPath = c.get('session').get('backToPath') as string || '/' + if (!backToPath.startsWith('/')) + backToPath = `/${backToPath}` + + return c.redirect(`${env.FRONTEND_URL!}${backToPath}`) + }) + + .get('/logout', async (c) => { + const logoutUrl = await kindeClient.logout(getSessionManager(c)) + return c.redirect(logoutUrl.toString()) + }) + + .get('/isAuth', async (c) => { + const isAuthenticated = await kindeClient.isAuthenticated(getSessionManager(c)) // Boolean: true or false + return c.json(isAuthenticated) + }) + + .get('/profile', async (c) => { + const profile = await kindeClient.getUserProfile(getSessionManager(c)) + return c.json(profile) + }) + + .get('/createOrg', async (c) => { + const org_name = c.req.query('org_name')?.toString() + const createUrl = await kindeClient.createOrg(getSessionManager(c), { org_name }) + return c.redirect(createUrl.toString()) + }) + + .get('/getOrg', async (c) => { + const org = await kindeClient.getOrganization(getSessionManager(c)) + return c.json(org) + }) + + .get('/getOrgs', async (c) => { + const orgs = await kindeClient.getUserOrganizations(getSessionManager(c)) + return c.json(orgs) + }) + + .get('/getPerm/:perm', async (c) => { + const perm = await kindeClient.getPermission(getSessionManager(c), c.req.param('perm')) + return c.json(perm) + }) + + .get('/getPerms', async (c) => { + const perms = await kindeClient.getPermissions(getSessionManager(c)) + return c.json(perms) + }) + + // Try: /api/auth/getClaim/aud, /api/auth/getClaim/email/id_token + .get('/getClaim/:claim', async (c) => { + const type = (c.req.query('type') ?? 'access_token') as ClaimTokenType + if (!/^(?:access_token|id_token)$/.test(type)) + return c.text('Bad request: type', 400) + + const claim = await kindeClient.getClaim(getSessionManager(c), c.req.param('claim'), type) + return c.json(claim) + }) + + .get('/getFlag/:code', async (c) => { + const claim = await kindeClient.getFlag( + getSessionManager(c), + c.req.param('code'), + c.req.query('default'), + c.req.query('flagType') as keyof FlagType | undefined, + ) + return c.json(claim) + }) + + .get('/getToken', async (c) => { + const accessToken = await kindeClient.getToken(getSessionManager(c)) + return c.text(accessToken) + }) diff --git a/apps/backend/src/api/greet.ts b/apps/backend/src/api/greet.ts new file mode 100644 index 0000000..7212e6c --- /dev/null +++ b/apps/backend/src/api/greet.ts @@ -0,0 +1,16 @@ +import { type } from 'arktype' + +import { appFactory } from '~/factory' +import { customArktypeValidator } from '~/middlewares/arktype' + +export const greetRouteApp = appFactory.createApp() + .get( + '', + customArktypeValidator('query', type({ + name: 'string>0', + })), + async (c) => { + const { name } = c.req.valid('query') + return c.text(`Hello ${name}!`) + }, + ) diff --git a/apps/backend/src/api/hello.helper.ts b/apps/backend/src/api/hello.helper.ts new file mode 100644 index 0000000..d72630d --- /dev/null +++ b/apps/backend/src/api/hello.helper.ts @@ -0,0 +1,2 @@ +// This isa sample for structuring guide +export const getHelloMessage = () => `Hello from Hono! - ${Date.now()}` diff --git a/apps/backend/src/api/hello.ts b/apps/backend/src/api/hello.ts new file mode 100644 index 0000000..3f27055 --- /dev/null +++ b/apps/backend/src/api/hello.ts @@ -0,0 +1,6 @@ +import { appFactory } from '~/factory' + +import { getHelloMessage } from './hello.helper' + +export const helloRouteApp = appFactory.createApp() + .get('', async c => c.text(getHelloMessage())) diff --git a/apps/backend/src/dev.ts b/apps/backend/src/dev.ts index 3c74889..05ed04b 100644 --- a/apps/backend/src/dev.ts +++ b/apps/backend/src/dev.ts @@ -1,5 +1,6 @@ import { env, isDevelopment } from 'std-env' import type { Hono, MiddlewareHandler } from 'hono' + import { logger } from '~/logger' // TODO: This middleware populate AWS context in local development diff --git a/apps/backend/src/factory.ts b/apps/backend/src/factory.ts index a6a554b..3f8dca9 100644 --- a/apps/backend/src/factory.ts +++ b/apps/backend/src/factory.ts @@ -1,5 +1,6 @@ import { createFactory } from 'hono/factory' import { createTriggerFactory } from 'hono-adapter-aws-lambda' + import type { HonoEnv } from '~/types' export const appFactory = createFactory() diff --git a/apps/backend/src/api/auth/sessionManager.ts b/apps/backend/src/helpers/kinde.ts similarity index 100% rename from apps/backend/src/api/auth/sessionManager.ts rename to apps/backend/src/helpers/kinde.ts index 1331320..940b6d0 100644 --- a/apps/backend/src/api/auth/sessionManager.ts +++ b/apps/backend/src/helpers/kinde.ts @@ -1,7 +1,7 @@ +import { HTTPException } from 'hono/http-exception' import type { SessionManager } from '@kinde-oss/kinde-typescript-sdk' import type { Context } from 'hono' import type { Session } from 'hono-sessions' -import { HTTPException } from 'hono/http-exception' /** * This is a wrapper on top of hono-sessions for Kinde compatibility diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index c1776e0..a49837d 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,15 +1,15 @@ -import { handle } from 'hono-adapter-aws-lambda' import { cors } from 'hono/cors' -import { logger as loggerMiddleware } from 'hono/logger' import { HTTPException } from 'hono/http-exception' +import { logger as loggerMiddleware } from 'hono/logger' +import { handle } from 'hono-adapter-aws-lambda' import { env, isDevelopment } from 'std-env' -import { apiApp } from './api/app' - +import { devAdapter, tryServeApp } from '~/dev' import { appFactory, triggerFactory } from '~/factory' import { logger } from '~/logger' import { cookieSession } from '~/middlewares/session' -import { devAdapter, tryServeApp } from '~/dev' + +import { apiApp } from './api/app' const _app = appFactory.createApp() // Registers an adapter middleware for development only diff --git a/apps/backend/src/logger.ts b/apps/backend/src/logger.ts index 876ec30..65b9040 100644 --- a/apps/backend/src/logger.ts +++ b/apps/backend/src/logger.ts @@ -1,5 +1,5 @@ +import { createConsola, LogLevels } from 'consola' import { isDevelopment } from 'std-env' -import { LogLevels, createConsola } from 'consola' export const logger = createConsola( { diff --git a/apps/backend/src/helpers/arktype.ts b/apps/backend/src/middlewares/arktype.ts similarity index 100% rename from apps/backend/src/helpers/arktype.ts rename to apps/backend/src/middlewares/arktype.ts diff --git a/apps/backend/src/middlewares/session.ts b/apps/backend/src/middlewares/session.ts index 7751231..63b625d 100644 --- a/apps/backend/src/middlewares/session.ts +++ b/apps/backend/src/middlewares/session.ts @@ -160,15 +160,15 @@ export async function headerSession(options: Record = {}) { sessionData = await store.getSessionById(sessionId) } }) +} - async function _jwtResolver() { - const { verify } = await import('hono/jwt') +async function _jwtResolver() { + const { verify } = await import('hono/jwt') - return async (value: string, c: Context) => { - const payload = await verify(value, 'top-secret') - c.set('jwtPayload', payload) + return async (value: string, c: Context) => { + const payload = await verify(value, 'top-secret') + c.set('jwtPayload', payload) - return payload.id as string - } + return payload.id as string } } diff --git a/apps/backend/src/api/auth/kindeClients.ts b/apps/backend/src/providers/auth/kinde-main.ts similarity index 91% rename from apps/backend/src/api/auth/kindeClients.ts rename to apps/backend/src/providers/auth/kinde-main.ts index 45fab12..c00a241 100644 --- a/apps/backend/src/api/auth/kindeClients.ts +++ b/apps/backend/src/providers/auth/kinde-main.ts @@ -1,4 +1,4 @@ -import { GrantType, createKindeServerClient } from '@kinde-oss/kinde-typescript-sdk' +import { createKindeServerClient, GrantType } from '@kinde-oss/kinde-typescript-sdk' import { env } from 'std-env' // Client for authorization code flow diff --git a/apps/frontend/app/lib/components/ui/button/Button.vue b/apps/frontend/app/lib/components/ui/button/Button.vue index 5cfd668..9905bc6 100644 --- a/apps/frontend/app/lib/components/ui/button/Button.vue +++ b/apps/frontend/app/lib/components/ui/button/Button.vue @@ -1,8 +1,8 @@