├── .gitignore ├── .npmignore ├── .swcrc ├── README.md ├── bun.lockb ├── package.json ├── src ├── add │ ├── bearer │ │ └── index.ts │ ├── cookie │ │ └── index.ts │ ├── cors │ │ └── index.ts │ ├── graphql │ │ ├── apollo │ │ │ └── index.ts │ │ ├── index.ts │ │ └── yoga │ │ │ └── index.ts │ ├── html │ │ └── index.ts │ ├── index.ts │ ├── jwt │ │ └── index.ts │ ├── static │ │ └── index.ts │ └── swagger │ │ └── index.ts ├── elf.ts ├── generate │ ├── auth │ │ ├── drizzle │ │ │ ├── elysia.txt │ │ │ ├── index.ts │ │ │ ├── mysql-redis.txt │ │ │ ├── mysql.txt │ │ │ ├── planetscale-redis.txt │ │ │ ├── planetscale.txt │ │ │ ├── postgres-redis.txt │ │ │ ├── postgres.txt │ │ │ └── template.ts │ │ ├── index.ts │ │ ├── oauth │ │ │ └── index.ts │ │ └── prisma │ │ │ ├── code-redis.txt │ │ │ ├── code.txt │ │ │ ├── elysia.txt │ │ │ ├── index.ts │ │ │ └── template.ts │ ├── index.ts │ └── route │ │ └── index.ts └── utils │ ├── elysia.ts │ ├── format.ts │ ├── fs.ts │ ├── index.ts │ ├── package.ts │ └── store.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | bin -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | src 4 | .git 5 | .gitignore 6 | .swcrc 7 | bun.lockb 8 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "module": { 4 | "type": "commonjs" 5 | }, 6 | "jsc": { 7 | "parser": { 8 | "syntax": "typescript" 9 | }, 10 | "target": "es2020" 11 | }, 12 | "minify": false 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elf Elysia 2 | Convenient way to scaffold Elysia project 3 | 4 | To get start, install the CLI: 5 | ```bash 6 | bun add -g @elysiajs/elf 7 | 8 | // Verify installation 9 | elf version 10 | ``` 11 | 12 | ## add (a) 13 | Install multiple plugins automatically all at once, and starting point for plugin usage to specified file. 14 | 15 | ## generate (gen / g) 16 | Create a starting point for opininated redundant task, eg. authentication, route 17 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elysiajs/elf/8665ba81b7d2297e9e1ff09a09d3e84e9deaf89e/bun.lockb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elysiajs/elf", 3 | "description": "Convenient way to scaffold Elysia project", 4 | "version": "0.0.0-exp-20230721.1301", 5 | "author": { 6 | "name": "saltyAom", 7 | "url": "https://github.com/SaltyAom", 8 | "email": "saltyaom@gmail.com" 9 | }, 10 | "main": "./bin/elf.js", 11 | "bin": { 12 | "elf": "./bin/elf.js" 13 | }, 14 | "scripts": { 15 | "build": "swc ./src -d bin --copy-files", 16 | "release": "npm run build && npm publish --access public" 17 | }, 18 | "dependencies": { 19 | "@inquirer/select": "^1.2.1", 20 | "chalk": "4.1.2", 21 | "inquirer": "8.2.5", 22 | "minimist": "^1.2.8", 23 | "prettier": "^2.8.8", 24 | "tasuku": "^2.0.1", 25 | "zx-cjs": "^7.0.7-0" 26 | }, 27 | "devDependencies": { 28 | "@swc/cli": "^0.1.62", 29 | "@swc/core": "^1.3.66", 30 | "@types/inquirer": "^9.0.3", 31 | "@types/minimist": "^1.2.2", 32 | "@types/prettier": "^2.7.3", 33 | "bun-types": "^0.6.9", 34 | "rimraf": "4.4.1", 35 | "typescript": "^5.1.3" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/elysiajs/elf" 40 | }, 41 | "bugs": "https://github.com/elysiajs/elf/issues", 42 | "homepage": "https://github.com/elysiajs/elf", 43 | "keywords": [ 44 | "bun", 45 | "http", 46 | "web", 47 | "server" 48 | ], 49 | "license": "MIT" 50 | } 51 | -------------------------------------------------------------------------------- /src/add/bearer/index.ts: -------------------------------------------------------------------------------- 1 | import { appendElysiaPlugin } from '../../utils' 2 | 3 | const header = `import { bearer } from '@elysiajs/bearer'` 4 | 5 | const code = `.use(bearer())` 6 | 7 | export const dependencies = ['@elysiajs/bearer'] 8 | 9 | const bearer = async () => { 10 | await appendElysiaPlugin({ 11 | header, 12 | code 13 | }) 14 | } 15 | 16 | export default bearer 17 | -------------------------------------------------------------------------------- /src/add/cookie/index.ts: -------------------------------------------------------------------------------- 1 | import { appendElysiaPlugin } from '../../utils' 2 | 3 | const header = `import { cookie } from '@elysiajs/cookie'` 4 | 5 | const code = `.use(cookie())` 6 | 7 | export const dependencies = ['@elysiajs/cookie'] 8 | 9 | const cookie = async () => { 10 | await appendElysiaPlugin( 11 | { 12 | header, 13 | code 14 | }, 15 | { 16 | duplicatable: false 17 | } 18 | ) 19 | } 20 | 21 | export default cookie 22 | -------------------------------------------------------------------------------- /src/add/cors/index.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | 3 | import { appendElysiaPlugin } from '../../utils' 4 | 5 | const header = `import { cors } from '@elysiajs/cors'` 6 | 7 | const code = `.use(cors())` 8 | 9 | export const dependencies = ['@elysiajs/cors'] 10 | 11 | const cors = async () => { 12 | await appendElysiaPlugin({ 13 | header, 14 | code 15 | }) 16 | } 17 | 18 | export default cors 19 | -------------------------------------------------------------------------------- /src/add/graphql/apollo/index.ts: -------------------------------------------------------------------------------- 1 | import { appendElysiaPlugin } from '../../../utils' 2 | 3 | const header = `import { apollo, gql } from '@elysiajs/apollo'` 4 | 5 | const code = `.use( 6 | apollo({ 7 | typeDefs: gql\` 8 | type Book { 9 | title: String 10 | author: String 11 | } 12 | 13 | type Query { 14 | books: [Book] 15 | } 16 | \`, 17 | resolvers: { 18 | Query: { 19 | books: () => { 20 | return [ 21 | { 22 | title: 'Elysia', 23 | author: 'saltyAom' 24 | } 25 | ] 26 | } 27 | } 28 | } 29 | }) 30 | )` 31 | 32 | export const dependencies = ['@elysiajs/apollo', 'graphql', '@apollo/server'] 33 | 34 | const apollo = async () => { 35 | await appendElysiaPlugin( 36 | { 37 | header, 38 | code 39 | }, 40 | { 41 | duplicatable: false 42 | } 43 | ) 44 | } 45 | 46 | export default apollo 47 | -------------------------------------------------------------------------------- /src/add/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import select, { Separator } from '@inquirer/select' 2 | 3 | import apollo, { dependencies as apolloDeps } from './apollo' 4 | import yoga, { dependencies as YogaDeps } from './yoga' 5 | 6 | import { install, store } from '../../utils' 7 | 8 | const graphql = async () => { 9 | const orm = await select({ 10 | message: 'GraphQL Library', 11 | choices: [ 12 | { 13 | name: 'Yoga', 14 | value: 'yoga' 15 | }, 16 | { 17 | name: 'Apollo', 18 | value: 'apollo' 19 | } 20 | ] 21 | }) 22 | 23 | switch (orm) { 24 | case 'apollo': 25 | store.deps.push(...apolloDeps) 26 | await apollo() 27 | break 28 | 29 | case 'yoga': 30 | store.deps.push(...YogaDeps) 31 | await yoga() 32 | break 33 | } 34 | } 35 | 36 | export default graphql 37 | -------------------------------------------------------------------------------- /src/add/graphql/yoga/index.ts: -------------------------------------------------------------------------------- 1 | import { appendElysiaPlugin } from '../../../utils' 2 | 3 | const header = `import { yoga } from '@elysiajs/graphql-yoga'` 4 | 5 | const code = `.use( 6 | yoga({ 7 | typeDefs: /* GraphQL */\` 8 | type Query { 9 | hi: String 10 | } 11 | \`, 12 | resolvers: { 13 | Query: { 14 | hi: () => 'Hello from Elysia' 15 | } 16 | } 17 | }) 18 | )` 19 | 20 | export const dependencies = [ 21 | '@elysiajs/graphql-yoga', 22 | 'graphql', 23 | 'graphql-yoga' 24 | ] 25 | 26 | const yoga = async () => { 27 | await appendElysiaPlugin( 28 | { 29 | header, 30 | code 31 | }, 32 | { 33 | duplicatable: false 34 | } 35 | ) 36 | } 37 | 38 | export default yoga 39 | -------------------------------------------------------------------------------- /src/add/html/index.ts: -------------------------------------------------------------------------------- 1 | import { appendElysiaPlugin } from '../../utils' 2 | 3 | const header = `import { html } from '@elysiajs/html'` 4 | 5 | const code = `.use(html())` 6 | 7 | export const dependencies = ['@elysiajs/html'] 8 | 9 | const html = async () => { 10 | await appendElysiaPlugin({ 11 | header, 12 | code 13 | }, { 14 | mainOnly: true 15 | }) 16 | } 17 | 18 | export default html 19 | -------------------------------------------------------------------------------- /src/add/index.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | import chalk from 'chalk' 3 | 4 | import bearer, { dependencies as bearerDeps } from './bearer' 5 | import cookie, { dependencies as cookieDeps } from './cookie' 6 | import cors, { dependencies as corsDeps } from './cors' 7 | import graphql from './graphql' 8 | import html, { dependencies as htmlDeps } from './html' 9 | import jwt, { dependencies as jwtDeps } from './jwt' 10 | import staticPlugin, { dependencies as staticPluginDeps } from './static' 11 | import swagger, { dependencies as swaggerDeps } from './swagger' 12 | 13 | import { indent, capitalize, store } from '../utils' 14 | 15 | const plugins = [ 16 | 'bearer', 17 | 'cookie', 18 | 'cors', 19 | 'graphql', 20 | 'html', 21 | 'jwt', 22 | 'static', 23 | 'swagger' 24 | ] as const 25 | 26 | export const depsMap = { 27 | bearer: bearerDeps, 28 | cookie: cookieDeps, 29 | cors: corsDeps, 30 | html: htmlDeps, 31 | jwt: jwtDeps, 32 | static: staticPluginDeps, 33 | staticPlugin: staticPluginDeps, 34 | swagger: swaggerDeps 35 | } as const 36 | 37 | export const addOptions = async () => { 38 | const { actions } = await i.prompt({ 39 | name: 'actions', 40 | message: 'Plugins to install', 41 | type: 'checkbox', 42 | choices: plugins.map((name) => ({ name })) 43 | }) 44 | 45 | return actions as string[] 46 | } 47 | 48 | const add = async (action: string) => { 49 | if (!plugins.includes(action as any)) 50 | return console.log( 51 | 'Plugin ' + chalk.red(action) + ' not found, skip...' 52 | ) 53 | 54 | const deps = depsMap[action as keyof typeof depsMap] 55 | if (deps?.length) store.deps.push(...deps) 56 | 57 | indent(`${action === 'html' ? 'HTML' : capitalize(action)} plugin`) 58 | 59 | switch (action) { 60 | case 'bearer': 61 | await bearer() 62 | break 63 | 64 | case 'cookie': 65 | await cookie() 66 | break 67 | 68 | case 'cors': 69 | await cors() 70 | break 71 | 72 | case 'graphql': 73 | await graphql() 74 | break 75 | 76 | case 'html': 77 | await html() 78 | break 79 | 80 | case 'jwt': 81 | await jwt() 82 | break 83 | 84 | case 'static': 85 | await staticPlugin() 86 | break 87 | 88 | case 'swagger': 89 | await swagger() 90 | break 91 | } 92 | } 93 | 94 | export default add 95 | -------------------------------------------------------------------------------- /src/add/jwt/index.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | 3 | import { existsSync } from 'fs' 4 | import { readFile, writeFile } from 'fs/promises' 5 | 6 | import { appendElysiaPlugin } from '../../utils' 7 | 8 | const header = `import { jwt } from '@elysiajs/jwt'` 9 | 10 | const code = (name: string, secret: string) => `.use( 11 | jwt({ 12 | name: '${name}', 13 | secret: process.env.JWT_SECRET! 14 | }) 15 | )` 16 | 17 | export const dependencies = ['@elysiajs/jwt'] 18 | 19 | const jwt = async () => { 20 | const { name } = await i.prompt({ 21 | name: 'name', 22 | message: 'Register JWT function as', 23 | default: 'jwt' 24 | }) 25 | 26 | let secret = '' 27 | let dotenv = '' 28 | 29 | if (existsSync('.env')) { 30 | dotenv = await readFile('.env', { 31 | encoding: 'utf8' 32 | }) 33 | 34 | const jwtSecret = dotenv.match(/JWT_SECRET=(.*?)(\n|$)/g) 35 | 36 | if (jwtSecret) secret = jwtSecret[0] 37 | } 38 | 39 | if (!secret) { 40 | const { secret: input } = await i.prompt({ 41 | name: 'secret', 42 | message: 'Secrets for encryption?', 43 | validate(secret) { 44 | if (!secret) return "Secret can't be empty" 45 | 46 | return true 47 | } 48 | }) 49 | 50 | secret = input 51 | 52 | const env = dotenv.includes('JWT_SECRET') 53 | ? dotenv.replace( 54 | /JWT_SECRET=(.*?)(\n|$)/g, 55 | `JWT_SECRET=${secret}\n` 56 | ) 57 | : dotenv + '\n' + `JWT_SECRET=${secret}` 58 | 59 | await writeFile('.env', env) 60 | } 61 | 62 | await appendElysiaPlugin({ 63 | header, 64 | code: code(name, secret) 65 | }) 66 | } 67 | 68 | export default jwt 69 | -------------------------------------------------------------------------------- /src/add/static/index.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | 3 | import { existsSync } from 'fs' 4 | import { readFile, writeFile, mkdir } from 'fs/promises' 5 | 6 | import { appendElysiaPlugin, mkdirRecursive } from '../../utils' 7 | 8 | const header = `import { staticPlugin } from '@elysiajs/static'` 9 | 10 | const code = (assets: string, prefix: string) => 11 | assets === 'public' && prefix === '/public' 12 | ? `.use(staticPlugin())` 13 | : `.use( 14 | staticPlugin({ 15 | ${assets === 'public' ? '' : `assets: ${assets},\n`}${ 16 | prefix === '/public' ? '' : `prefix: ${prefix}` 17 | } 18 | }) 19 | )` 20 | 21 | export const dependencies = ['@elysiajs/static'] 22 | 23 | const staticPlugin = async () => { 24 | const { assets } = await i.prompt({ 25 | name: 'assets', 26 | message: 'Folder to expose as public', 27 | default: 'public' 28 | }) 29 | 30 | mkdirRecursive(assets) 31 | 32 | const { prefix } = await i.prompt({ 33 | name: 'prefix', 34 | message: 'Prefix for exposed folder', 35 | default: '/public' 36 | }) 37 | 38 | await appendElysiaPlugin( 39 | { 40 | header, 41 | code: code(assets, prefix) 42 | }, 43 | { 44 | mainOnly: true 45 | } 46 | ) 47 | } 48 | 49 | export default staticPlugin 50 | -------------------------------------------------------------------------------- /src/add/swagger/index.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | 3 | import { appendElysiaPlugin } from '../../utils' 4 | 5 | const header = `import { swagger } from '@elysiajs/swagger'` 6 | 7 | const code = (title: string) => `.use( 8 | swagger({ 9 | documentation: { 10 | info: { 11 | title: '${title}', 12 | version: '0.1.0' 13 | } 14 | } 15 | }) 16 | )` 17 | 18 | export const dependencies = ['@elysiajs/swagger'] 19 | 20 | const swagger = async () => { 21 | const { title } = await i.prompt({ 22 | name: 'title', 23 | default: 'Elysia documentation' 24 | }) 25 | 26 | await appendElysiaPlugin( 27 | { 28 | header, 29 | code: code(title) 30 | }, 31 | { 32 | mainOnly: true, 33 | duplicatable: false 34 | } 35 | ) 36 | } 37 | 38 | export default swagger 39 | -------------------------------------------------------------------------------- /src/elf.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import select from '@inquirer/select' 3 | import i from 'inquirer' 4 | 5 | import add, { addOptions } from './add' 6 | import generate, { generateOptions } from './generate' 7 | 8 | import { install } from './utils' 9 | 10 | import type { ParsedArgs } from 'minimist' 11 | 12 | import info from '../package.json' 13 | import chalk from 'chalk' 14 | 15 | const argv: ParsedArgs = require('minimist')(process.argv.slice(2)) 16 | let { 17 | _: [action, ...subs] 18 | } = argv 19 | 20 | const main = async () => { 21 | if (!action) 22 | action = await select({ 23 | message: 'command', 24 | choices: [ 25 | { 26 | value: 'add', 27 | description: 'Add plugins to server' 28 | }, 29 | { 30 | value: 'generate', 31 | description: 'Generate starting files for development' 32 | } 33 | ] 34 | }) 35 | 36 | if (!subs.length) 37 | switch (action) { 38 | case 'a': 39 | case 'add': 40 | subs = await addOptions() 41 | break 42 | 43 | case 'g': 44 | case 'gen': 45 | case 'generate': 46 | subs = [await generateOptions()] 47 | break 48 | 49 | case 'v': 50 | case 'version': 51 | subs = [''] 52 | } 53 | 54 | let cmdNotFound = false 55 | 56 | for (const sub of subs) { 57 | switch (action) { 58 | case 'a': 59 | case 'add': 60 | await add(sub) 61 | break 62 | 63 | case 'g': 64 | case 'gen': 65 | case 'generate': 66 | await generate(sub) 67 | break 68 | 69 | case 'v': 70 | case 'version': 71 | console.log( 72 | chalk.cyanBright.bold('Elf Elysia') + ' - ' + info.description 73 | ) 74 | console.log('version: ' + info.version) 75 | 76 | cmdNotFound = true 77 | 78 | break 79 | 80 | default: 81 | cmdNotFound = true 82 | console.log(`${chalk.bold(action)} command not found`) 83 | } 84 | } 85 | 86 | if (!cmdNotFound) { 87 | await install() 88 | 89 | console.log('\n✅ All Set') 90 | } 91 | } 92 | 93 | main() 94 | -------------------------------------------------------------------------------- /src/generate/auth/drizzle/elysia.txt: -------------------------------------------------------------------------------- 1 | export const auth = (app: Elysia) => 2 | app.group('/auth', (app) => 3 | app 4 | .use(cookie()) 5 | .model( 6 | 'auth', 7 | t.Object({ 8 | username: t.String(), 9 | password: t.String({ 10 | minLength: 8 11 | }) 12 | }) 13 | ) 14 | .put( 15 | '/sign-up', 16 | async ({ body: { username, password } }) => 17 | lucia.createUser({ 18 | primaryKey: { 19 | providerId: 'username', 20 | providerUserId: username, 21 | password 22 | }, 23 | attributes: { 24 | username 25 | } 26 | }), 27 | { 28 | body: 'auth' 29 | } 30 | ) 31 | .post( 32 | '/sign-in', 33 | async ({ set, setCookie, body: { username, password } }) => { 34 | try { 35 | const { userId } = await lucia.useKey( 36 | 'username', 37 | username, 38 | password 39 | ) 40 | 41 | const { sessionId } = await lucia.createSession(userId) 42 | setCookie('session', sessionId) 43 | 44 | return `Sign in as ${username}` 45 | } catch { 46 | set.status = 401 47 | 48 | return 'Invalid username or password' 49 | } 50 | }, 51 | { 52 | body: 'auth' 53 | } 54 | ) 55 | .onBeforeHandle(lucia.sessionGuard) 56 | .get( 57 | '/refresh', 58 | async ({ cookie: { session }, setCookie }) => { 59 | const { sessionId: id } = await lucia.renewSession(session) 60 | 61 | setCookie('session', id) 62 | 63 | return session 64 | } 65 | ) 66 | .get( 67 | '/sign-out', 68 | async ({ cookie: { session }, removeCookie }) => { 69 | await lucia.invalidateSession(session) 70 | 71 | removeCookie('session') 72 | 73 | return session 74 | } 75 | ) 76 | .derive(async ({ cookie: { session } }) => { 77 | const { userId } = await lucia.getSession(session) 78 | 79 | return { 80 | userId 81 | } 82 | }) 83 | .get( 84 | '/profile', 85 | async ({ userId }) => db 86 | .select() 87 | .from(user) 88 | .where(eq(user.id, id)) 89 | ) 90 | .delete( 91 | '/user', 92 | async ({ userId, cookie: { session } }) => { 93 | await lucia.deleteDeadUserSessions(session) 94 | await lucia.deleteUser(userId) 95 | 96 | return userId 97 | } 98 | ) 99 | ) 100 | 101 | export default auth 102 | -------------------------------------------------------------------------------- /src/generate/auth/drizzle/index.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | import select, { Separator } from '@inquirer/select' 3 | import c from 'chalk' 4 | import { $, cd } from 'zx-cjs' 5 | 6 | import task from 'tasuku' 7 | 8 | import { existsSync } from 'fs' 9 | import { readFile, writeFile } from 'fs/promises' 10 | 11 | import { DrizzleTemplate } from './template' 12 | 13 | import { 14 | appendElysiaPlugin, 15 | store, 16 | install, 17 | mkdirRecursive, 18 | format 19 | } from '../../../utils' 20 | import oauth from '../oauth' 21 | 22 | const shared = 'drizzle-orm @elysiajs/cookie @elysiajs/lucia-auth' 23 | 24 | $.verbose = false 25 | 26 | export const drizzle = async () => { 27 | const { useRedis } = await i.prompt({ 28 | name: 'useRedis', 29 | type: 'confirm', 30 | message: 'Use Redis for storing session', 31 | default: true 32 | }) 33 | 34 | const database = (await select({ 35 | message: 'Database Client', 36 | choices: [ 37 | { 38 | name: 'Postgres', 39 | value: 'postgres' 40 | }, 41 | { 42 | name: 'MySQL', 43 | value: 'mysql' 44 | }, 45 | { 46 | name: 'PlanetScale', 47 | value: 'planetscale' 48 | } 49 | ] 50 | })) as 'postgres' | 'mysql' | 'planetscale' 51 | 52 | const { dependencies, devDependencies, code, env } = 53 | DrizzleTemplate[database] 54 | 55 | if (useRedis) 56 | store.deps.push(...['redis', '@lucia-auth/adapter-session-redis']) 57 | 58 | store.deps.push(...dependencies) 59 | 60 | if (devDependencies.length) store.deps.push(...devDependencies) 61 | 62 | await install() 63 | 64 | const hasEnv = existsSync('.env') 65 | 66 | const dotenv = hasEnv 67 | ? await readFile('.env', { 68 | encoding: 'utf8' 69 | }) 70 | : '' 71 | 72 | if (!dotenv.includes('DATABASE_URL=')) { 73 | const { url } = await i.prompt({ 74 | name: 'url', 75 | message: 'Provide DATABASE_URL', 76 | default: env 77 | }) 78 | 79 | if (!hasEnv) await writeFile('.env', `DATABASE_URL=${url}\n`) 80 | else if (url) { 81 | await writeFile( 82 | '.env', 83 | dotenv.replace( 84 | /DATABASE_URL=(.*?)(\n|$)/g, 85 | `DATABASE_URL=${url}\n` 86 | ) 87 | ) 88 | } 89 | } 90 | 91 | const { prefix } = await i.prompt({ 92 | name: 'prefix', 93 | message: 'Path prefix', 94 | default: '/auth' 95 | }) 96 | 97 | const oauthCode = await oauth(prefix) 98 | 99 | const { file } = await i.prompt({ 100 | name: 'file', 101 | message: 'File location?', 102 | default: 'src/auth.ts', 103 | validate(file) { 104 | if (existsSync(file)) return 'File exists' 105 | 106 | return true 107 | } 108 | }) 109 | 110 | mkdirRecursive(file) 111 | 112 | await writeFile( 113 | file, 114 | await format( 115 | code(useRedis) + 116 | '\n' + 117 | DrizzleTemplate.elysia(prefix).replace( 118 | '.use(cookie())', 119 | `.use(cookie())${oauthCode}` 120 | ) 121 | ) 122 | ) 123 | 124 | await appendElysiaPlugin( 125 | { 126 | header: `import auth from '${file 127 | .replace(/^src\//, './') 128 | .replace(/.(j|t)s/, '')}'`, 129 | code: '.use(auth)' 130 | }, 131 | { 132 | mainOnly: true, 133 | duplicatable: false 134 | } 135 | ) 136 | } 137 | 138 | export default drizzle 139 | -------------------------------------------------------------------------------- /src/generate/auth/drizzle/mysql-redis.txt: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { cookie } from '@elysiajs/cookie' 3 | 4 | import { Lucia } from '@elysiajs/lucia-auth' 5 | import { mysql2 } from '@lucia-auth/adapter-mysql' 6 | 7 | import mysql from 'mysql2/promise' 8 | import { eq } from 'drizzle-orm' 9 | import { mysqlTable, bigint, varchar, boolean } from 'drizzle-orm/mysql-core' 10 | import { drizzle } from 'drizzle-orm/mysql2' 11 | 12 | import redis from '@lucia-auth/adapter-session-redis' 13 | import { createClient } from 'redis' 14 | 15 | export const user = mysqlTable('auth_user', { 16 | id: varchar('id', { 17 | // change this when using custom user ids 18 | length: 15 19 | }).primaryKey() 20 | // other user attributes 21 | }) 22 | 23 | export const session = mysqlTable('auth_session', { 24 | id: varchar('id', { 25 | length: 128 26 | }).primaryKey(), 27 | userId: varchar('user_id', { 28 | length: 15 29 | }) 30 | .notNull() 31 | .references(() => user.id), 32 | activeExpires: bigint('active_expires', { 33 | mode: 'number' 34 | }).notNull(), 35 | idleExpires: bigint('idle_expires', { 36 | mode: 'number' 37 | }).notNull() 38 | }) 39 | 40 | export const key = mysqlTable('auth_key', { 41 | id: varchar('id', { 42 | length: 255 43 | }).primaryKey(), 44 | userId: varchar('user_id', { 45 | length: 15 46 | }) 47 | .notNull() 48 | .references(() => user.id), 49 | primaryKey: boolean('primary_key').notNull(), 50 | hashedPassword: varchar('hashed_password', { 51 | length: 255 52 | }), 53 | expires: bigint('expires', { 54 | mode: 'number' 55 | }) 56 | }) 57 | 58 | const connectionPool = mysql.createPool({ 59 | // 'mysql://user:pass@host' 60 | uri: process.env.DATABASE_URL 61 | }) 62 | 63 | const db = drizzle(connectionPool) 64 | 65 | const userSessionClient = createClient() 66 | const sessionClient = createClient() 67 | 68 | await userSessionClient.connect() 69 | await sessionClient.connect() 70 | 71 | const lucia = Lucia({ 72 | adapter: { 73 | user: mysql2(connectionPool), 74 | session: redis({ 75 | session: sessionClient, 76 | userSession: userSessionClient 77 | }) 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /src/generate/auth/drizzle/mysql.txt: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { cookie } from '@elysiajs/cookie' 3 | 4 | import { Lucia } from '@elysiajs/lucia-auth' 5 | import { mysql2 } from '@lucia-auth/adapter-mysql' 6 | 7 | import mysql from 'mysql2/promise' 8 | import { eq } from 'drizzle-orm' 9 | import { mysqlTable, bigint, varchar, boolean } from 'drizzle-orm/mysql-core' 10 | import { drizzle } from 'drizzle-orm/mysql2' 11 | 12 | export const user = mysqlTable('auth_user', { 13 | id: varchar('id', { 14 | // change this when using custom user ids 15 | length: 15 16 | }).primaryKey() 17 | // other user attributes 18 | }) 19 | 20 | export const session = mysqlTable('auth_session', { 21 | id: varchar('id', { 22 | length: 128 23 | }).primaryKey(), 24 | userId: varchar('user_id', { 25 | length: 15 26 | }) 27 | .notNull() 28 | .references(() => user.id), 29 | activeExpires: bigint('active_expires', { 30 | mode: 'number' 31 | }).notNull(), 32 | idleExpires: bigint('idle_expires', { 33 | mode: 'number' 34 | }).notNull() 35 | }) 36 | 37 | export const key = mysqlTable('auth_key', { 38 | id: varchar('id', { 39 | length: 255 40 | }).primaryKey(), 41 | userId: varchar('user_id', { 42 | length: 15 43 | }) 44 | .notNull() 45 | .references(() => user.id), 46 | primaryKey: boolean('primary_key').notNull(), 47 | hashedPassword: varchar('hashed_password', { 48 | length: 255 49 | }), 50 | expires: bigint('expires', { 51 | mode: 'number' 52 | }) 53 | }) 54 | 55 | const connectionPool = mysql.createPool({ 56 | // 'mysql://user:pass@host' 57 | uri: process.env.DATABASE_URL 58 | }) 59 | 60 | const db = drizzle(connectionPool) 61 | 62 | const lucia = Lucia({ 63 | adapter: mysql2(connectionPool) 64 | }) 65 | -------------------------------------------------------------------------------- /src/generate/auth/drizzle/planetscale-redis.txt: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { cookie } from '@elysiajs/cookie' 3 | 4 | import { Lucia } from '@elysiajs/lucia-auth' 5 | import { planetscale } from '@lucia-auth/adapter-mysql' 6 | 7 | import { connect } from '@planetscale/database' 8 | import { eq } from 'drizzle-orm' 9 | import { mysqlTable, bigint, varchar, boolean } from 'drizzle-orm/mysql-core' 10 | import { drizzle } from 'drizzle-orm/planetscale-serverless' 11 | 12 | import redis from '@lucia-auth/adapter-session-redis' 13 | import { createClient } from 'redis' 14 | 15 | export const user = mysqlTable('auth_user', { 16 | id: varchar('id', { 17 | // change this when using custom user ids 18 | length: 15 19 | }).primaryKey() 20 | // other user attributes 21 | }) 22 | 23 | export const session = mysqlTable('auth_session', { 24 | id: varchar('id', { 25 | length: 128 26 | }).primaryKey(), 27 | userId: varchar('user_id', { 28 | length: 15 29 | }) 30 | .notNull() 31 | .references(() => user.id), 32 | activeExpires: bigint('active_expires', { 33 | mode: 'number' 34 | }).notNull(), 35 | idleExpires: bigint('idle_expires', { 36 | mode: 'number' 37 | }).notNull() 38 | }) 39 | 40 | export const key = mysqlTable('auth_key', { 41 | id: varchar('id', { 42 | length: 255 43 | }).primaryKey(), 44 | userId: varchar('user_id', { 45 | length: 15 46 | }) 47 | .notNull() 48 | .references(() => user.id), 49 | primaryKey: boolean('primary_key').notNull(), 50 | hashedPassword: varchar('hashed_password', { 51 | length: 255 52 | }), 53 | expires: bigint('expires', { 54 | mode: 'number' 55 | }) 56 | }) 57 | 58 | const connection = connect({ 59 | // 'mysql://user:pass@host' 60 | url: process.env.DATABASE_URL 61 | }) 62 | 63 | const db = drizzle(connection) 64 | 65 | const userSessionClient = createClient() 66 | const sessionClient = createClient() 67 | 68 | await userSessionClient.connect() 69 | await sessionClient.connect() 70 | 71 | const lucia = Lucia({ 72 | adapter: { 73 | user: planetscale(connection), 74 | session: redis({ 75 | session: sessionClient, 76 | userSession: userSessionClient 77 | }) 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /src/generate/auth/drizzle/planetscale.txt: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { cookie } from '@elysiajs/cookie' 3 | 4 | import { Lucia } from '@elysiajs/lucia-auth' 5 | import { planetscale } from '@lucia-auth/adapter-mysql' 6 | 7 | import { connect } from '@planetscale/database' 8 | import { eq } from 'drizzle-orm' 9 | import { mysqlTable, bigint, varchar, boolean } from 'drizzle-orm/mysql-core' 10 | import { drizzle } from 'drizzle-orm/planetscale-serverless' 11 | 12 | export const user = mysqlTable('auth_user', { 13 | id: varchar('id', { 14 | // change this when using custom user ids 15 | length: 15 16 | }).primaryKey() 17 | // other user attributes 18 | }) 19 | 20 | export const session = mysqlTable('auth_session', { 21 | id: varchar('id', { 22 | length: 128 23 | }).primaryKey(), 24 | userId: varchar('user_id', { 25 | length: 15 26 | }) 27 | .notNull() 28 | .references(() => user.id), 29 | activeExpires: bigint('active_expires', { 30 | mode: 'number' 31 | }).notNull(), 32 | idleExpires: bigint('idle_expires', { 33 | mode: 'number' 34 | }).notNull() 35 | }) 36 | 37 | export const key = mysqlTable('auth_key', { 38 | id: varchar('id', { 39 | length: 255 40 | }).primaryKey(), 41 | userId: varchar('user_id', { 42 | length: 15 43 | }) 44 | .notNull() 45 | .references(() => user.id), 46 | primaryKey: boolean('primary_key').notNull(), 47 | hashedPassword: varchar('hashed_password', { 48 | length: 255 49 | }), 50 | expires: bigint('expires', { 51 | mode: 'number' 52 | }) 53 | }) 54 | 55 | const connection = connect({ 56 | // 'mysql://user:pass@host' 57 | url: process.env.DATABASE_URL 58 | }) 59 | 60 | const db = drizzle(connection) 61 | 62 | const lucia = Lucia({ 63 | adapter: planetscale(connection) 64 | }) 65 | -------------------------------------------------------------------------------- /src/generate/auth/drizzle/postgres-redis.txt: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { cookie } from '@elysiajs/cookie' 3 | 4 | import { Lucia } from '@elysiajs/lucia-auth' 5 | import { pg } from '@lucia-auth/adapter-postgresql' 6 | 7 | import postgres from 'pg' 8 | import { eq } from 'drizzle-orm' 9 | import { drizzle } from 'drizzle-orm/node-postgres' 10 | import { pgTable, bigint, varchar, boolean } from 'drizzle-orm/pg-core' 11 | 12 | import redis from '@lucia-auth/adapter-session-redis' 13 | import { createClient } from 'redis' 14 | 15 | const user = pgTable('auth_user', { 16 | id: varchar('id', { 17 | // change this when using custom user ids 18 | length: 15 19 | }).primaryKey() 20 | // other user attributes 21 | }) 22 | 23 | const session = pgTable('auth_session', { 24 | id: varchar('id', { 25 | length: 128 26 | }).primaryKey(), 27 | userId: varchar('user_id', { 28 | length: 15 29 | }) 30 | .notNull() 31 | .references(() => user.id), 32 | activeExpires: bigint('active_expires', { 33 | mode: 'number' 34 | }).notNull(), 35 | idleExpires: bigint('idle_expires', { 36 | mode: 'number' 37 | }).notNull() 38 | }) 39 | 40 | const key = pgTable('auth_key', { 41 | id: varchar('id', { 42 | length: 255 43 | }).primaryKey(), 44 | userId: varchar('user_id', { 45 | length: 15 46 | }) 47 | .notNull() 48 | .references(() => user.id), 49 | primaryKey: boolean('primary_key').notNull(), 50 | hashedPassword: varchar('hashed_password', { 51 | length: 255 52 | }), 53 | expires: bigint('expires', { 54 | mode: 'number' 55 | }) 56 | }) 57 | 58 | const connectionPool = new postgres.Pool({ 59 | connectionString: process.env.DATABASE_URL 60 | }) 61 | 62 | const db = drizzle(connectionPool) 63 | 64 | const userSessionClient = createClient() 65 | const sessionClient = createClient() 66 | 67 | await userSessionClient.connect() 68 | await sessionClient.connect() 69 | 70 | const lucia = Lucia({ 71 | adapter: { 72 | user: pg(connectionPool), 73 | session: redis({ 74 | session: sessionClient, 75 | userSession: userSessionClient 76 | }) 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /src/generate/auth/drizzle/postgres.txt: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { cookie } from '@elysiajs/cookie' 3 | 4 | import { Lucia } from '@elysiajs/lucia-auth' 5 | import { pg } from '@lucia-auth/adapter-postgresql' 6 | 7 | import postgres from 'pg' 8 | import { eq } from 'drizzle-orm' 9 | import { drizzle } from 'drizzle-orm/node-postgres' 10 | import { pgTable, bigint, varchar, boolean } from 'drizzle-orm/pg-core' 11 | 12 | const user = pgTable('auth_user', { 13 | id: varchar('id', { 14 | // change this when using custom user ids 15 | length: 15 16 | }).primaryKey() 17 | // other user attributes 18 | }) 19 | 20 | const session = pgTable('auth_session', { 21 | id: varchar('id', { 22 | length: 128 23 | }).primaryKey(), 24 | userId: varchar('user_id', { 25 | length: 15 26 | }) 27 | .notNull() 28 | .references(() => user.id), 29 | activeExpires: bigint('active_expires', { 30 | mode: 'number' 31 | }).notNull(), 32 | idleExpires: bigint('idle_expires', { 33 | mode: 'number' 34 | }).notNull() 35 | }) 36 | 37 | const key = pgTable('auth_key', { 38 | id: varchar('id', { 39 | length: 255 40 | }).primaryKey(), 41 | userId: varchar('user_id', { 42 | length: 15 43 | }) 44 | .notNull() 45 | .references(() => user.id), 46 | primaryKey: boolean('primary_key').notNull(), 47 | hashedPassword: varchar('hashed_password', { 48 | length: 255 49 | }), 50 | expires: bigint('expires', { 51 | mode: 'number' 52 | }) 53 | }) 54 | 55 | const connectionPool = new postgres.Pool({ 56 | connectionString: process.env.DATABASE_URL 57 | }) 58 | 59 | const db = drizzle(connectionPool) 60 | 61 | const lucia = Lucia({ 62 | adapter: pg(connectionPool) 63 | }) 64 | -------------------------------------------------------------------------------- /src/generate/auth/drizzle/template.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | 3 | export const DrizzleTemplate = { 4 | postgres: { 5 | dependencies: ['@lucia-auth/adapter-postgresql'], 6 | devDependencies: ['@types/pg'], 7 | env: 'postgresql://postgres:12345678@localhost:5432/mydb', 8 | code: (useRedis: boolean) => 9 | readFileSync( 10 | __dirname + 11 | (useRedis ? '/postgres-redis.txt' : '/postgres.txt'), 12 | { 13 | encoding: 'utf8' 14 | } 15 | ) 16 | }, 17 | mysql: { 18 | dependencies: ['better-sqlite3', '@lucia-auth/adapter-mysql'], 19 | devDependencies: ['@types/better-sqlite3'], 20 | env: 'mysql://user:pass@localhost:3306/mydb', 21 | code: (useRedis: boolean) => 22 | readFileSync( 23 | __dirname + (useRedis ? '/mysql-redis.txt' : '/mysql.txt'), 24 | { 25 | encoding: 'utf8' 26 | } 27 | ) 28 | }, 29 | planetscale: { 30 | dependencies: ['@planetscale/database', '@lucia-auth/adapter-mysql'], 31 | devDependencies: [], 32 | env: 'mysql://user:pass@localhost:3306/mydb', 33 | code: (useRedis: boolean) => 34 | readFileSync( 35 | __dirname + 36 | (useRedis ? '/planetscale-redis.txt' : '/planetscale.txt'), 37 | { 38 | encoding: 'utf8' 39 | } 40 | ) 41 | }, 42 | elysia: (prefix: string) => 43 | readFileSync(__dirname + '/elysia.txt', { 44 | encoding: 'utf8' 45 | }).replace('/auth', prefix) 46 | } 47 | -------------------------------------------------------------------------------- /src/generate/auth/index.ts: -------------------------------------------------------------------------------- 1 | import select, { Separator } from '@inquirer/select' 2 | 3 | import { prisma } from './prisma' 4 | import { drizzle } from './drizzle' 5 | 6 | const auth = async () => { 7 | const orm = await select({ 8 | message: 'Database Client', 9 | choices: [ 10 | { 11 | name: 'Prisma', 12 | value: 'prisma' 13 | }, 14 | { 15 | name: 'Drizzle', 16 | value: 'drizzle' 17 | }, 18 | { 19 | name: 'Kysely', 20 | value: 'kysely', 21 | disabled: true 22 | } 23 | ] 24 | }) 25 | 26 | switch (orm) { 27 | case 'prisma': 28 | await prisma() 29 | break 30 | 31 | case 'drizzle': 32 | await drizzle() 33 | break 34 | } 35 | } 36 | 37 | export default auth 38 | -------------------------------------------------------------------------------- /src/generate/auth/oauth/index.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | import select, { Separator } from '@inquirer/select' 3 | import c from 'chalk' 4 | 5 | import { readFile, writeFile } from 'fs/promises' 6 | 7 | import { capitalize } from '../../../utils' 8 | 9 | const oauthOptions = [ 10 | 'discord', 11 | 'facebook', 12 | 'github', 13 | 'google', 14 | 'linkedin', 15 | 'patreon', 16 | 'reddit', 17 | 'twitch' 18 | ] as const 19 | 20 | const ask = (message: string) => 21 | i 22 | .prompt({ 23 | name: 'value', 24 | message 25 | }) 26 | .then(({ value }) => value) 27 | 28 | const createOauthEnv = async (name: string) => ` 29 | ${name.toUpperCase()}_CLIENT_ID=${await ask(`${capitalize(name)} Client ID`)} 30 | ${name.toUpperCase()}_CLIENT_SECRET=${await ask(`${capitalize(name)} Secret`)}` 31 | 32 | const createOauthCode = (name: string, prefix = '/auth') => `.use( 33 | lucia.oauth.${name}({ 34 | config: { 35 | clientId: process.env.${name.toUpperCase()}_CLIENT_ID!, 36 | clientSecret: process.env.${name.toUpperCase()}_CLIENT_SECRET!, 37 | redirectUri: 'http://localhost:3000${prefix}/${name}/callback' 38 | } 39 | }) 40 | )` 41 | 42 | const appendOAuth = async (name: string) => { 43 | const dotenv = await readFile('.env', { 44 | encoding: 'utf8' 45 | }) 46 | 47 | if (!dotenv.includes(`${name.toUpperCase()}_CLIENT_ID=`)) 48 | await writeFile( 49 | '.env', 50 | dotenv + 51 | (dotenv.endsWith('\n') ? '' : '\n') + 52 | (await createOauthEnv(name)) 53 | ) 54 | } 55 | 56 | const oauth = async (prefix = '/auth') => { 57 | const { oauth } = await i.prompt({ 58 | name: 'oauth', 59 | message: 'Select OAuth Provider (optional)', 60 | type: 'checkbox', 61 | choices: oauthOptions.map((value) => ({ value })) 62 | }) 63 | 64 | let code = '' 65 | 66 | for (const auth of oauth) { 67 | await appendOAuth(auth) 68 | 69 | code += createOauthCode(auth) 70 | } 71 | 72 | return code 73 | } 74 | 75 | export default oauth 76 | -------------------------------------------------------------------------------- /src/generate/auth/prisma/code-redis.txt: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { cookie } from '@elysiajs/cookie' 3 | 4 | import { Lucia } from '@elysiajs/lucia-auth' 5 | import prismaAdapter from '@lucia-auth/adapter-prisma' 6 | 7 | import { PrismaClient } from '@prisma/client' 8 | 9 | import redis from '@lucia-auth/adapter-session-redis' 10 | import { createClient } from 'redis' 11 | 12 | const prisma = new PrismaClient() 13 | 14 | const userSessionClient = createClient() 15 | const sessionClient = createClient() 16 | 17 | await userSessionClient.connect() 18 | await sessionClient.connect() 19 | 20 | const lucia = Lucia({ 21 | adapter: { 22 | user: prismaAdapter(prisma as any), 23 | session: redis({ 24 | session: sessionClient, 25 | userSession: userSessionClient 26 | }) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /src/generate/auth/prisma/code.txt: -------------------------------------------------------------------------------- 1 | import { Elysia, t } from 'elysia' 2 | import { cookie } from '@elysiajs/cookie' 3 | 4 | import { Lucia } from '@elysiajs/lucia-auth' 5 | import prismaAdapter from '@lucia-auth/adapter-prisma' 6 | 7 | import { PrismaClient } from '@prisma/client' 8 | 9 | const prisma = new PrismaClient() 10 | 11 | const lucia = Lucia({ 12 | adapter: prismaAdapter(prisma as any) 13 | }) 14 | -------------------------------------------------------------------------------- /src/generate/auth/prisma/elysia.txt: -------------------------------------------------------------------------------- 1 | export const auth = (app: Elysia) => 2 | app.group('/auth', (app) => 3 | app 4 | .use(cookie()) 5 | .model( 6 | 'auth', 7 | t.Object({ 8 | username: t.String(), 9 | password: t.String({ 10 | minLength: 8 11 | }) 12 | }) 13 | ) 14 | .put( 15 | '/sign-up', 16 | async ({ body: { username, password } }) => 17 | lucia.createUser({ 18 | primaryKey: { 19 | providerId: 'username', 20 | providerUserId: username, 21 | password 22 | }, 23 | attributes: { 24 | username 25 | } 26 | }), 27 | { 28 | body: 'auth' 29 | } 30 | ) 31 | .post( 32 | '/sign-in', 33 | async ({ set, setCookie, body: { username, password } }) => { 34 | try { 35 | const { userId } = await lucia.useKey( 36 | 'username', 37 | username, 38 | password 39 | ) 40 | 41 | const { sessionId } = await lucia.createSession(userId) 42 | setCookie('session', sessionId) 43 | 44 | return `Sign in as ${username}` 45 | } catch { 46 | set.status = 401 47 | 48 | return 'Invalid username or password' 49 | } 50 | }, 51 | { 52 | body: 'auth' 53 | } 54 | ) 55 | .onBeforeHandle(lucia.sessionGuard) 56 | .get( 57 | '/refresh', 58 | async ({ cookie: { session }, setCookie }) => { 59 | const { sessionId: id } = await lucia.renewSession(session) 60 | 61 | setCookie('session', id) 62 | 63 | return session 64 | } 65 | ) 66 | .get( 67 | '/sign-out', 68 | async ({ cookie: { session }, removeCookie }) => { 69 | await lucia.invalidateSession(session) 70 | 71 | removeCookie('session') 72 | 73 | return session 74 | } 75 | ) 76 | .derive(async ({ cookie: { session } }) => { 77 | const { userId } = await lucia.getSession(session) 78 | 79 | return { 80 | userId 81 | } 82 | }) 83 | .get( 84 | '/profile', 85 | async ({ userId }) => prisma.authUser.findUnique({ 86 | where: { 87 | id: userId 88 | } 89 | }) 90 | ) 91 | .delete( 92 | '/user', 93 | async ({ userId, cookie: { session } }) => { 94 | await lucia.deleteDeadUserSessions(session) 95 | await lucia.deleteUser(userId) 96 | 97 | return userId 98 | } 99 | ) 100 | ) 101 | 102 | export default auth 103 | -------------------------------------------------------------------------------- /src/generate/auth/prisma/index.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | import select, { Separator } from '@inquirer/select' 3 | import c from 'chalk' 4 | import { $, cd } from 'zx-cjs' 5 | 6 | import task from 'tasuku' 7 | 8 | import { existsSync } from 'fs' 9 | import { readFile, writeFile } from 'fs/promises' 10 | 11 | import { PrismaTemplate } from './template' 12 | 13 | import { 14 | format, 15 | appendElysiaPlugin, 16 | store, 17 | install, 18 | mkdirRecursive 19 | } from '../../../utils' 20 | import oauth from '../oauth' 21 | 22 | $.verbose = false 23 | 24 | export const prisma = async () => { 25 | const { useRedis } = await i.prompt({ 26 | name: 'useRedis', 27 | type: 'confirm', 28 | message: 'Use Redis for storing session', 29 | default: true 30 | }) 31 | 32 | if (useRedis) 33 | store.deps.push(...['redis', '@lucia-auth/adapter-session-redis']) 34 | 35 | store.deps.push( 36 | ...[ 37 | '@prisma/client', 38 | '@lucia-auth/adapter-prisma', 39 | '@elysiajs/cookie', 40 | '@elysiajs/lucia-auth' 41 | ] 42 | ) 43 | 44 | store.devs.push('prisma') 45 | 46 | await install() 47 | 48 | try { 49 | await $`bunx prisma init` 50 | } catch {} 51 | 52 | // ? Prisma init already generate .env with johndoe:randompassword 53 | 54 | const dotenv = await readFile('.env', { 55 | encoding: 'utf8' 56 | }) 57 | 58 | let schema = await readFile('prisma/schema.prisma', { 59 | encoding: 'utf8' 60 | }) 61 | 62 | const database = await select({ 63 | message: 'Database provider', 64 | choices: [ 65 | { 66 | value: 'postgres' 67 | }, 68 | { 69 | value: 'mysql' 70 | }, 71 | { 72 | value: 'planetscale' 73 | }, 74 | { 75 | value: 'cockroachdb' 76 | }, 77 | { 78 | value: 'mongodb' 79 | }, 80 | { 81 | value: 'sqlite' 82 | }, 83 | { 84 | value: 'sqlserver' 85 | } 86 | ] 87 | } as const) 88 | 89 | if (database === 'planetscale') 90 | schema = schema.replace( 91 | 'datasource db {', 92 | 'datasource db {\n relationMode = "prisma"' 93 | ) 94 | 95 | schema = schema.replace( 96 | 'provider = "postgres"', 97 | `provider = "${database === 'planetscale' ? 'mysql' : database}"` 98 | ) 99 | 100 | if (dotenv.includes('://johndoe:randompassword')) { 101 | const { url } = await i.prompt({ 102 | name: 'url', 103 | message: 'Provide DATABASE_URL', 104 | default: PrismaTemplate.connection[database] 105 | }) 106 | 107 | await writeFile( 108 | '.env', 109 | dotenv.replace(/DATABASE_URL=(.*?)(\n|$)/g, `DATABASE_URL=${url}\n`) 110 | ) 111 | } 112 | 113 | if (!schema.includes('model AuthUser')) { 114 | await writeFile( 115 | 'prisma/schema.prisma', 116 | schema + 117 | (database === 'mongodb' 118 | ? PrismaTemplate.schemaMongo 119 | : PrismaTemplate.schema) 120 | ) 121 | 122 | const { shouldMigrate } = await i.prompt({ 123 | name: 'shouldMigrate', 124 | type: 'confirm', 125 | message: 'Migrate database?', 126 | default: true 127 | }) 128 | 129 | if (shouldMigrate) { 130 | $.verbose = true 131 | try { 132 | await $`bunx prisma migrate dev --name init` 133 | } catch {} 134 | $.verbose = false 135 | } 136 | } 137 | 138 | await $`bunx prisma generate` 139 | 140 | const { prefix } = await i.prompt({ 141 | name: 'prefix', 142 | message: 'Path prefix', 143 | default: '/auth' 144 | }) 145 | 146 | const oauthCode = await oauth(prefix) 147 | 148 | const { file } = await i.prompt({ 149 | name: 'file', 150 | message: 'File location?', 151 | default: 'src/auth.ts', 152 | validate(file) { 153 | if (existsSync(file)) return 'File exists' 154 | 155 | return true 156 | } 157 | }) 158 | 159 | mkdirRecursive(file) 160 | 161 | await writeFile( 162 | file, 163 | await format( 164 | PrismaTemplate.code(useRedis) + 165 | '\n' + 166 | PrismaTemplate.elysia(prefix).replace( 167 | '.use(cookie())', 168 | `.use(cookie())${oauthCode}` 169 | ) 170 | ) 171 | ) 172 | 173 | await appendElysiaPlugin( 174 | { 175 | header: `import auth from '${file 176 | .replace(/^src\//, './') 177 | .replace(/.(j|t)s/, '')}'`, 178 | code: '.use(auth)' 179 | }, 180 | { 181 | mainOnly: true, 182 | duplicatable: false 183 | } 184 | ) 185 | } 186 | 187 | export default prisma 188 | -------------------------------------------------------------------------------- /src/generate/auth/prisma/template.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | 3 | export const PrismaTemplate = { 4 | schema: ` 5 | model AuthUser { 6 | id String @id @unique 7 | auth_session AuthSession[] 8 | auth_key AuthKey[] 9 | 10 | username String @unique 11 | 12 | // Change table name here 13 | @@map("auth_user") 14 | } 15 | 16 | model AuthSession { 17 | id String @id @unique 18 | user_id String 19 | active_expires BigInt 20 | idle_expires BigInt 21 | auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) 22 | 23 | @@index([user_id]) 24 | @@map("auth_session") 25 | } 26 | 27 | model AuthKey { 28 | id String @id @unique 29 | hashed_password String? 30 | user_id String 31 | primary_key Boolean 32 | expires BigInt? 33 | auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) 34 | 35 | @@index([user_id]) 36 | @@map("auth_key") 37 | } 38 | `, 39 | 40 | schemaMongo: ` 41 | model AuthUser { 42 | id String @id @map("_id") 43 | auth_session AuthSession[] 44 | auth_key AuthKey[] 45 | 46 | username String @unique 47 | 48 | // Change table name here 49 | @@map("auth_user") 50 | } 51 | 52 | model AuthSession { 53 | id String @id @map("_id") 54 | user_id String 55 | active_expires BigInt 56 | idle_expires BigInt 57 | auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) 58 | 59 | @@index([user_id]) 60 | @@map("auth_session") 61 | } 62 | 63 | model AuthKey { 64 | id String @id @map("_id") 65 | hashed_password String? 66 | user_id String 67 | primary_key Boolean 68 | expires BigInt? 69 | auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) 70 | 71 | @@index([user_id]) 72 | @@map("auth_key") 73 | } 74 | `, 75 | 76 | connection: { 77 | 'postgres': 'postgresql://postgres:12345678@localhost:5432/mydb?schema=public', 78 | 'mysql': 'mysql://mysql:12345678@localhost:3306/mydb?schema=public', 79 | 'planetscale': 'mysql://platnetscale:3306@localhost:5432/mydb?schema=public', 80 | 'cockroachdb': 'postgresql://cockroach:26257@localhost:5432/mydb?schema=public', 81 | 'mongodb': 'mongodb://mongo:12345678@localhost:27017/mydb?ssl=true&connectTimeoutMS=5000&maxPoolSize=50', 82 | 'sqlite': 'file:./dev.db', 83 | 'sqlserver': 'sqlserver://localhost:1433;database=mydb;user=sqlserver;password=12345678;encrypt=true', 84 | }, 85 | code: (useRedis: boolean) => 86 | readFileSync(__dirname + (useRedis ? '/code-redis.txt' : '/code.txt'), { 87 | encoding: 'utf8' 88 | }), 89 | 90 | elysia: (prefix: string) => 91 | readFileSync(__dirname + '/elysia.txt', { 92 | encoding: 'utf8' 93 | }).replace('/auth', prefix) 94 | } 95 | -------------------------------------------------------------------------------- /src/generate/index.ts: -------------------------------------------------------------------------------- 1 | import select from '@inquirer/select' 2 | import chalk from 'chalk' 3 | 4 | import auth from './auth' 5 | import route from './route' 6 | 7 | import { indent, capitalize } from '../utils/format' 8 | 9 | const generators = ['auth', 'route'] as const 10 | 11 | export const generateOptions = () => 12 | select({ 13 | message: 'generate', 14 | choices: generators.map((value) => ({ value })) 15 | }) 16 | 17 | const generate = async (action: string) => { 18 | if (!generators.includes(action as any)) 19 | return console.log( 20 | 'Generator for ' + chalk.red(action) + ' not found, skip...' 21 | ) 22 | 23 | indent(`Generate ${capitalize(action)}`) 24 | 25 | switch (action) { 26 | case 'auth': 27 | await auth() 28 | break 29 | 30 | case 'route': 31 | await route() 32 | break 33 | } 34 | } 35 | 36 | export default generate 37 | -------------------------------------------------------------------------------- /src/generate/route/index.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | 3 | import { existsSync } from 'fs' 4 | import { writeFile } from 'fs/promises' 5 | 6 | import add, { addOptions } from '../../add' 7 | 8 | import { 9 | appendElysiaPlugin, 10 | mkdirRecursive, 11 | prettier, 12 | store 13 | } from '../../utils' 14 | 15 | const route = async () => { 16 | let code = `import type { Elysia } from 'elysia'\n\n` 17 | 18 | const { name } = await i.prompt({ 19 | name: 'name', 20 | message: 'Route name?', 21 | validate(name) { 22 | if (!name) return "Route name can't be empty" 23 | 24 | return true 25 | } 26 | }) 27 | 28 | code += `const ${name} = (app: Elysia) => app` 29 | 30 | const { prefix } = await i.prompt({ 31 | name: 'prefix', 32 | message: 'Path prefix?' 33 | }) 34 | 35 | if (prefix) code += `.group('${prefix}', (app) => app` 36 | 37 | code += `.get('/', () => 'hello ${name}')` 38 | 39 | // May something be here 40 | 41 | if (prefix) code += ')' 42 | 43 | code += `\n\nexport default ${name}\n` 44 | 45 | const { file } = await i.prompt({ 46 | name: 'file', 47 | message: 'File location?', 48 | default: existsSync('src') 49 | ? `src/${name}/index.ts` 50 | : `${name}/index.ts`, 51 | validate(file) { 52 | if (existsSync(file)) return 'File exists' 53 | 54 | return true 55 | } 56 | }) 57 | 58 | mkdirRecursive(file) 59 | await writeFile(file, code) 60 | 61 | store.file = file 62 | 63 | const options = await addOptions() 64 | 65 | if (options.length) for (const option of options) await add(option) 66 | else await prettier(file) 67 | } 68 | 69 | export default route 70 | -------------------------------------------------------------------------------- /src/utils/elysia.ts: -------------------------------------------------------------------------------- 1 | import i from 'inquirer' 2 | 3 | import { existsSync } from 'fs' 4 | import { readFile, writeFile } from 'fs/promises' 5 | 6 | import { format, resolveConfig } from 'prettier' 7 | 8 | import { store } from './store' 9 | import { prettier } from './format' 10 | 11 | const mainFile = [ 12 | 'src/index.ts', 13 | 'index.ts', 14 | 'src/main.ts', 15 | 'main.ts', 16 | 'src/index.js', 17 | 'index.js', 18 | 'src/main.js', 19 | 'main.js' 20 | ] 21 | 22 | const insert = (word: string, text: string, index: number) => 23 | text.slice(0, index) + word + text.slice(index) 24 | 25 | export const appendElysiaPlugin = async ( 26 | word: 27 | | string 28 | | { 29 | header?: string 30 | code?: string 31 | }, 32 | { 33 | duplicatable, 34 | mainOnly, 35 | file: requestedFile 36 | }: { 37 | file?: string 38 | duplicatable?: boolean 39 | mainOnly?: boolean 40 | } = { 41 | file: '', 42 | duplicatable: false, 43 | mainOnly: false 44 | } 45 | ) => { 46 | const { header = '', code = '' } = 47 | typeof word === 'object' 48 | ? word 49 | : { 50 | header: '', 51 | code: word 52 | } 53 | 54 | let file = 55 | requestedFile || store.file || mainFile.find((file) => existsSync(file)) 56 | 57 | if (!store.file && (!mainOnly || !file)) { 58 | const { file: prompted } = await i.prompt({ 59 | name: 'file', 60 | message: 'Append to', 61 | type: 'input', 62 | default: file, 63 | validate(answer) { 64 | return existsSync(answer) ? true : 'File not found' 65 | } 66 | }) 67 | 68 | file = prompted 69 | } 70 | 71 | file = file as string 72 | 73 | let content = await readFile(file, { 74 | encoding: 'utf8' 75 | }) 76 | 77 | let instance = store.file ? '' : content.match(/new Elysia(<(.*)>)?\(/g) 78 | let codeStart = content.indexOf(')', content.indexOf('new Elysia')) + 1 79 | 80 | if (!instance) 81 | (() => { 82 | const arrowReturn = content.match( 83 | /(var|let|const) (\w)+ = (\()?(\w)+(: (\w)+)?(\)?) =>(\ |\t|\n)+(\w)+/g 84 | ) 85 | 86 | if (arrowReturn?.[0]) 87 | return (codeStart = 88 | content.indexOf(arrowReturn[0]) + arrowReturn[0].length) 89 | 90 | const [, params] = 91 | content.match( 92 | /(var|let|const|function) \w( )?(=)?( )?(\()?(\w)+(: (\w)+)?(\)?)/g 93 | ) ?? [] 94 | 95 | if (!params) { 96 | console.log( 97 | `Unable to find main Elysia instance or plugin, make sure you have either 'const app = new Elysia()' or 'const plugin = (app) => app' initialized in ${file}` 98 | ) 99 | 100 | process.exit(1) 101 | } 102 | 103 | codeStart = content.indexOf(params) + params.length 104 | })() 105 | 106 | if (instance && instance.length > 1) { 107 | console.log( 108 | `Unable to find main Elysia instance, make sure you have only one 'new Elysia()' initialized in ${file}` 109 | ) 110 | process.exit(1) 111 | } 112 | 113 | if (code) content = insert(code, content, codeStart) 114 | 115 | if (header && !content.includes(header)) { 116 | const lastImport = content.lastIndexOf('import') 117 | 118 | const startIndex = !lastImport 119 | ? 0 120 | : content.indexOf('\n', lastImport) + 1 121 | 122 | content = insert(header + '\n', content, startIndex) 123 | } 124 | 125 | await prettier(file, content) 126 | } 127 | -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | import { bold } from 'chalk' 2 | 3 | import { existsSync } from 'fs' 4 | import { readFile, writeFile } from 'fs/promises' 5 | 6 | import { format as formatCode, resolveConfig } from 'prettier' 7 | 8 | export const indent = (word: string) => console.log(bold(`\n> ${word}`)) 9 | 10 | export const capitalize = (word: string) => 11 | word.charAt(0).toUpperCase() + word.slice(1) 12 | 13 | export const format = async (code: string) => 14 | formatCode(code, { 15 | parser: 'typescript', 16 | ...(await resolveConfig(process.cwd())) 17 | }) 18 | 19 | export const prettier = async ( 20 | file: string, 21 | content: string | Promise = readFile(file, { 22 | encoding: 'utf8' 23 | }) 24 | ) => writeFile(file, await format(await content)) 25 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'fs' 2 | 3 | export const mkdirRecursive = (path: string) => { 4 | let paths = path.split('/') 5 | 6 | // Determine file or folder 7 | // eg: src/controllers/salt.ts 8 | if (path.lastIndexOf('/') < path.lastIndexOf('.')) 9 | paths = paths.slice(0, paths.lastIndexOf('/')) 10 | 11 | paths.reduce((directories, directory) => { 12 | directories += `${directory}/` 13 | 14 | if (!existsSync(directories)) mkdirSync(directories) 15 | 16 | return directories 17 | }, '') 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { appendElysiaPlugin } from './elysia' 2 | export { indent, capitalize, format, prettier } from './format' 3 | export { mkdirRecursive } from './fs' 4 | export { install } from './package' 5 | export { store } from './store' 6 | -------------------------------------------------------------------------------- /src/utils/package.ts: -------------------------------------------------------------------------------- 1 | import task from 'tasuku' 2 | import { $ } from 'zx-cjs' 3 | 4 | import { readFileSync, existsSync } from 'fs' 5 | import { store } from './store' 6 | 7 | const packages = existsSync('package.json') 8 | ? (JSON.parse( 9 | readFileSync('package.json', { 10 | encoding: 'utf8' 11 | }) 12 | ) as { 13 | dependencies: Record 14 | devDependencies: Record 15 | }) 16 | : null 17 | 18 | const _deps = packages?.dependencies ? Object.keys(packages.dependencies) : [] 19 | const _devs = packages?.devDependencies 20 | ? Object.keys(packages.devDependencies) 21 | : [] 22 | 23 | export const install = async () => { 24 | const deps = [ 25 | ...new Set(store.deps.filter((name) => !_deps.includes(name))) 26 | ].join(' ') 27 | 28 | const devs = [ 29 | ...new Set(store.devs.filter((name) => !_devs.includes(name))) 30 | ].join(' ') 31 | 32 | if (_deps || _devs) { 33 | console.log('') 34 | await task('Installing dependencies', async () => { 35 | try { 36 | if (deps) { 37 | $.verbose = false 38 | await new Function('$', `return $\`bun add ${deps}\``)($) 39 | store.deps = [] 40 | } 41 | 42 | if (devs) { 43 | $.verbose = false 44 | await new Function('$', `return $\`bun add -d ${devs}\``)($) 45 | store.devs = [] 46 | } 47 | } catch (error) { 48 | console.log( 49 | 'Failed to install dependencies. Please install dependency yourself\n' 50 | ) 51 | 52 | console.log(`dependencies: ${deps}`) 53 | console.log(`devDependencies: ${devs}`) 54 | } 55 | }).then((x) => x.clear()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/store.ts: -------------------------------------------------------------------------------- 1 | export const store = { 2 | // Default file location to write to 3 | file: '', 4 | deps: [] as string[], 5 | devs: [] as string[] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "outDir": "bin", 5 | "lib": ["ESNext"], 6 | "module": "CommonJS", 7 | "target": "ES2021", 8 | "moduleResolution": "Node", 9 | "moduleDetection": "force", 10 | "strict": true, 11 | "downlevelIteration": true, 12 | "skipLibCheck": true, 13 | "jsx": "preserve", 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "resolveJsonModule": true, 17 | "types": [ 18 | "bun-types" // add Bun global 19 | ] 20 | } 21 | } 22 | --------------------------------------------------------------------------------