├── utils ├── index.ts ├── kv.ts ├── genToken.ts └── marked.ts ├── bun.lockb ├── screenshot.png ├── public ├── favicon.png ├── normalize.css └── github-markdown.css ├── controllers ├── index.ts ├── userControllers.ts └── memoController.ts ├── models ├── index.ts ├── memoModels.ts └── userModels.ts ├── routes ├── index.ts ├── userRoutes.tsx ├── memoRoutes.tsx └── pageRoutes.tsx ├── middlewares ├── index.ts ├── cacheMiddlewares.ts ├── errorMiddlewares.ts └── authMiddlewares.ts ├── Dockerfile ├── pages ├── layout │ ├── header.tsx │ ├── footer.tsx │ └── index.tsx ├── memo.tsx ├── styles │ └── index.ts └── index.tsx ├── package.json ├── .env.example ├── tsconfig.json ├── config └── db.ts ├── app.ts ├── .gitignore └── README.md /utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as genToken } from './genToken' -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakholuo/quest/HEAD/bun.lockb -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakholuo/quest/HEAD/screenshot.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakholuo/quest/HEAD/public/favicon.png -------------------------------------------------------------------------------- /controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * as user from './userControllers' 2 | export * as memo from './memoController' -------------------------------------------------------------------------------- /models/index.ts: -------------------------------------------------------------------------------- 1 | export { default as User } from './userModels' 2 | export { default as Memo } from './memoModels' -------------------------------------------------------------------------------- /utils/kv.ts: -------------------------------------------------------------------------------- 1 | 2 | import kvjs from '@heyputer/kv.js' 3 | 4 | const kv = new kvjs() 5 | 6 | export default kv; -------------------------------------------------------------------------------- /routes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Users } from './userRoutes' 2 | export { default as Pages } from './pageRoutes' 3 | export { default as Memos } from './memoRoutes' -------------------------------------------------------------------------------- /middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export { protect } from './authMiddlewares' 2 | export { errorHandler, notFound } from './errorMiddlewares' 3 | export { cache } from './cacheMiddlewares' -------------------------------------------------------------------------------- /routes/userRoutes.tsx: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { user } from '../controllers' 3 | 4 | const users = new Hono() 5 | 6 | users.post('/token', (c) => user.getToken(c)) 7 | 8 | export default users -------------------------------------------------------------------------------- /utils/genToken.ts: -------------------------------------------------------------------------------- 1 | import { sign } from 'hono/jwt' 2 | 3 | const genToken = (id: string, exp?: number) => { 4 | let payload: any = { id } 5 | if (exp) payload.exp = exp 6 | return sign(payload, Bun.env.JWT_SECRET || '') 7 | } 8 | 9 | export default genToken -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM oven/bun:1 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | COPY bun.lockb . 7 | 8 | RUN bun install 9 | 10 | COPY . . 11 | 12 | ENV NODE_ENV=production 13 | ENV PORT=8848 14 | 15 | EXPOSE 8848 16 | 17 | CMD ["bun", "run", "start"] 18 | 19 | -------------------------------------------------------------------------------- /pages/layout/header.tsx: -------------------------------------------------------------------------------- 1 | import { headerStyle } from "../styles" 2 | 3 | const Header = () => { 4 | return ( 5 |
6 |

{Bun.env.TITLE}

7 |

{Bun.env.DESCRIPTION}

8 |
9 | ) 10 | } 11 | 12 | export default Header -------------------------------------------------------------------------------- /middlewares/cacheMiddlewares.ts: -------------------------------------------------------------------------------- 1 | import { Context, Next } from 'hono' 2 | import kvjs from '../utils/kv' 3 | 4 | export const cache = async (c: Context, next: Next) => { 5 | 6 | try { 7 | c.set('kvjs', kvjs) 8 | await next() 9 | } catch (error) { 10 | await next() 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quest", 3 | "scripts": { 4 | "dev": "bun run --hot app.ts", 5 | "start": "bun run app.ts" 6 | }, 7 | "dependencies": { 8 | "@heyputer/kv.js": "^0.1.8", 9 | "dayjs": "^1.11.13", 10 | "hono": "^4.5.11", 11 | "marked": "^14.1.2", 12 | "mongoose": "^8.6.1" 13 | }, 14 | "devDependencies": { 15 | "@types/bun": "latest" 16 | } 17 | } -------------------------------------------------------------------------------- /utils/marked.ts: -------------------------------------------------------------------------------- 1 | import { marked } from 'marked' 2 | 3 | const renderer = new marked.Renderer() 4 | 5 | renderer.link = function(href: string, title: string, text: string) { 6 | var link = marked.Renderer.prototype.link.apply(this, arguments) 7 | return link.replace(" { 4 | return ( 5 | 8 | ) 9 | } 10 | 11 | export default Footer -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT="8848" 2 | MONGO_URI="mongodb://127.0.0.1:27017/quest" 3 | JWT_SECRET="your-secret-key" 4 | TOKEN_EXPIRE_TIME="0" 5 | ADMIN_USERNAME="admin" 6 | ADMIN_PASSWORD="admin" 7 | TITLE="Quest" 8 | SUB_TITLE="A Simple Memo Site" 9 | DESCRIPTION="Quest your interesting" 10 | KEYWORDS="Quest, Memo, Simple" 11 | INDEX_PAGE_SIZE="10" 12 | CACHE_SECONDS="3600" 13 | FONT_SCRIPT_URL="https://fonts.googleapis.com/css?family=Mono" 14 | UMAMI_URL="https://cloud.umami.is/script.js" 15 | UMAMI_WEBSITE_ID="" -------------------------------------------------------------------------------- /middlewares/errorMiddlewares.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'hono' 2 | 3 | // Error Handler 4 | export const errorHandler = (c: Context) => { 5 | c.status(500) 6 | return c.json({ 7 | success: false, 8 | message: c.error?.message, 9 | // stack: process.env.NODE_ENV === 'production' ? null : c.error?.stack, 10 | }) 11 | } 12 | 13 | // Not Found Handler 14 | export const notFound = (c: Context) => { 15 | c.status(404) 16 | return c.json({ 17 | success: false, 18 | message: `Not Found - [${c.req.method}] ${c.req.url}`, 19 | }) 20 | } -------------------------------------------------------------------------------- /models/memoModels.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema, model } from 'mongoose' 2 | 3 | interface IMemo { 4 | content: string 5 | tags: string[] 6 | createdAt: string 7 | } 8 | 9 | interface IMemoDoc extends IMemo, Document { 10 | mathPassword: (pass: string) => Promise 11 | } 12 | 13 | const memoSchema = new Schema( 14 | { 15 | content: { type: String, required: true }, 16 | tags: { type: [], required: true }, 17 | }, 18 | { 19 | timestamps: true, 20 | } 21 | ) 22 | 23 | const Memo = model('Memo', memoSchema) 24 | export default Memo -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "strict": true, 10 | "downlevelIteration": true, 11 | "skipLibCheck": true, 12 | "jsx": "preserve", 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "allowJs": true, 16 | "noEmit": true, 17 | "jsxImportSource": "hono/jsx", 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /routes/memoRoutes.tsx: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { memo } from '../controllers' 3 | import { protect } from '../middlewares' 4 | import { cache } from '../middlewares' 5 | 6 | type Variables = { 7 | kvjs: any 8 | } 9 | 10 | const memos = new Hono<{ Variables: Variables }>() 11 | 12 | memos.post('/create', protect, cache, (c) => memo.createMome(c)) 13 | memos.post('/list', cache, (c) => memo.findMomes(c)) 14 | memos.get('/tags', protect, (c) => memo.findTags(c)) 15 | memos.get('/get', protect, (c) => memo.findMomeById(c)) 16 | memos.post('/update', protect, cache, (c) => memo.updateMemo(c)) 17 | memos.post('/delete', protect, cache, (c) => memo.deleteMemo(c)) 18 | 19 | export default memos -------------------------------------------------------------------------------- /config/db.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose' 2 | import { User } from '../models' 3 | 4 | const connectDB = async () => { 5 | try { 6 | if (Bun.env.MONGO_URI !== undefined) { 7 | const conn = await mongoose.connect(Bun.env.MONGO_URI, { 8 | autoIndex: true, 9 | }) 10 | 11 | console.log(`MongoDB Connected: ${conn.connection.host}`) 12 | 13 | const userExists = await User.findOne({ name: Bun.env.ADMIN_USERNAME }); 14 | if (userExists) { 15 | return; 16 | } 17 | await User.create({ 18 | name: Bun.env.ADMIN_USERNAME, 19 | password: Bun.env.ADMIN_PASSWORD, 20 | }) 21 | console.log('Admin user created'); 22 | } 23 | } catch (err: any) { 24 | console.error(`Error: ${err.message}`) 25 | process.exit(1) 26 | } 27 | } 28 | 29 | export default connectDB -------------------------------------------------------------------------------- /pages/memo.tsx: -------------------------------------------------------------------------------- 1 | import { markdownBodyClass } from "./styles" 2 | import { Style } from "hono/css" 3 | import Layout from "./layout" 4 | 5 | interface IMemoProps { 6 | title: string 7 | keywords: string 8 | description: string 9 | memo: any 10 | } 11 | 12 | const Memo = (props: IMemoProps) => { 13 | const item = props.memo 14 | return ( 15 | 16 | 17 | 22 | 23 | ) 24 | } 25 | 26 | export default Memo 27 | -------------------------------------------------------------------------------- /models/userModels.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema, model } from 'mongoose' 2 | 3 | interface IUser { 4 | name: string 5 | password: string 6 | } 7 | 8 | interface IUserDoc extends IUser, Document { 9 | mathPassword: (pass: string) => Promise 10 | } 11 | 12 | const userSchema = new Schema( 13 | { 14 | name: { type: String, required: true }, 15 | password: { type: String, required: true }, 16 | }, 17 | { 18 | timestamps: true, 19 | } 20 | ) 21 | 22 | userSchema.methods.mathPassword = async function (enteredPassword: string) { 23 | return Bun.password.verifySync(enteredPassword, this.password) 24 | } 25 | 26 | userSchema.pre('save', async function (next) { 27 | if (!this.isModified('password')) { 28 | next() 29 | } 30 | 31 | this.password = await Bun.password.hash(this.password, { 32 | algorithm: 'bcrypt', 33 | cost: 4 34 | }) 35 | }) 36 | 37 | const User = model('User', userSchema) 38 | export default User -------------------------------------------------------------------------------- /middlewares/authMiddlewares.ts: -------------------------------------------------------------------------------- 1 | import { Context, Next } from 'hono' 2 | import { Jwt } from 'hono/utils/jwt' 3 | 4 | import { User } from '../models' 5 | 6 | export const protect = async (c: Context, next: Next) => { 7 | let token 8 | 9 | if ( 10 | c.req.header('Authorization') && 11 | c.req.header('Authorization')?.startsWith('Bearer') 12 | ) { 13 | try { 14 | token = c.req.header('Authorization')?.replace(/Bearer\s+/i, '') 15 | if (!token) { 16 | return c.json({ message: 'Not authorized to access this route' }) 17 | } 18 | 19 | const { id } = await Jwt.verify(token, Bun.env.JWT_SECRET || '') 20 | 21 | const user = await User.findById(id).select('-password') 22 | c.set('user', user) 23 | 24 | await next() 25 | } catch (err) { 26 | throw new Error('Invalid token! You are not authorized!') 27 | } 28 | } 29 | 30 | if (!token) { 31 | throw new Error('Not authorized! No token found!') 32 | } 33 | } -------------------------------------------------------------------------------- /controllers/userControllers.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'hono' 2 | import { User } from '../models' 3 | import { genToken } from '../utils' 4 | 5 | export const getToken = async (c: Context) => { 6 | const { name, password } = await c.req.json() 7 | 8 | if (!name || !password) { 9 | c.status(400) 10 | throw new Error('Please provide an name and password') 11 | } 12 | 13 | const user = await User.findOne({ name }) 14 | if (!user) { 15 | c.status(401) 16 | throw new Error('No user found with this name') 17 | } 18 | 19 | if (!(await user.mathPassword(password))) { 20 | c.status(401) 21 | throw new Error('Invalid credentials') 22 | } else { 23 | const token = await genToken(user._id!.toString(), Bun.env.TOKEN_EXPIRE_TIME === "0" ? 0 : Math.floor(Date.now() / 1000 + Number(Bun.env.TOKEN_EXPIRE_TIME))) 24 | 25 | return c.json({ 26 | success: true, 27 | data: { 28 | _id: user._id, 29 | name: user.name, 30 | }, 31 | token, 32 | message: 'User logged in successfully', 33 | }) 34 | } 35 | } -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from 'hono' 2 | import { logger } from 'hono/logger' 3 | import { serveStatic } from 'hono/bun' 4 | import { prettyJSON } from 'hono/pretty-json' 5 | import { compress } from 'hono/compress' 6 | import { cors } from 'hono/cors' 7 | import { Users, Pages, Memos } from './routes' 8 | import { errorHandler, notFound } from './middlewares' 9 | import connectDB from './config/db' 10 | 11 | const app = new Hono().basePath('/') 12 | 13 | connectDB() 14 | 15 | app.use('*', logger(), prettyJSON()) 16 | 17 | app.use( 18 | '*', 19 | cors({ 20 | origin: '*', 21 | allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 22 | }) 23 | ) 24 | 25 | app.use('/public/*', serveStatic({ root: './' })) 26 | 27 | app.route('/', Pages) 28 | app.route('/api/users', Users) 29 | app.route('/api/memo', Memos) 30 | 31 | app.onError((err, c) => { 32 | const error = errorHandler(c) 33 | return error 34 | }) 35 | 36 | app.notFound((c) => { 37 | const error = notFound(c) 38 | return error 39 | }) 40 | 41 | app.use(compress()) 42 | 43 | const port = Bun.env.PORT || 8848 44 | 45 | export default { 46 | port, 47 | fetch: app.fetch, 48 | } -------------------------------------------------------------------------------- /pages/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import Header from './header' 2 | import Footer from './footer' 3 | import { Style } from 'hono/css' 4 | import { bodyStyle } from '../styles' 5 | 6 | interface SiteData { 7 | title?: string 8 | keywords?: string 9 | description?: string 10 | image?: string 11 | children?: any 12 | } 13 | 14 | const Layout = (props: SiteData) => { 15 | return ( 16 | 17 | 18 | 19 | {props.title ? `${props.title} - ${Bun.env.TITLE}` : `${Bun.env.TITLE} - ${Bun.env.SUB_TITLE}` } 20 | 21 | 22 | 23 | 24 | 25 | 26 | { 27 | Bun.env.UMAMI_WEBSITE_ID && ( 28 | 29 | ) 30 | } 31 | { 32 | Bun.env.FONT_SCRIPT_URL && ( 33 | 34 | ) 35 | } 36 | 37 | 38 |
39 | {props?.children} 40 |