init
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.idea/
|
||||||
|
.env
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 140,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
15
README.md
Normal file
15
README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# chat
|
||||||
|
|
||||||
|
To install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
To run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||||
110
bun.lock
Normal file
110
bun.lock
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "chat",
|
||||||
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.14.3",
|
||||||
|
"@grpc/proto-loader": "^0.8.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"jose": "^6.1.3",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"prettier": "3.8.1",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
|
||||||
|
|
||||||
|
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
|
||||||
|
|
||||||
|
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
||||||
|
|
||||||
|
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||||
|
|
||||||
|
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||||
|
|
||||||
|
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
|
||||||
|
|
||||||
|
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||||
|
|
||||||
|
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
|
||||||
|
|
||||||
|
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||||
|
|
||||||
|
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||||
|
|
||||||
|
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||||
|
|
||||||
|
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||||
|
|
||||||
|
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||||
|
|
||||||
|
"@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||||
|
|
||||||
|
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||||
|
|
||||||
|
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
|
|
||||||
|
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
|
||||||
|
|
||||||
|
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||||
|
|
||||||
|
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "chat",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"prettier": "3.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.14.3",
|
||||||
|
"@grpc/proto-loader": "^0.8.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"jose": "^6.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
199
proto/message.proto
Normal file
199
proto/message.proto
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option go_package = "./";
|
||||||
|
|
||||||
|
package message;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
service MessageService {
|
||||||
|
|
||||||
|
rpc CreateUser(CreateUserRequest) returns(CreateUserResponse);
|
||||||
|
rpc UpdateUser(UpdateUserRequest) returns(UpdateUserResponse);
|
||||||
|
rpc GetUserByToken(GetUserByTokenRequest) returns(GetUserResponse);
|
||||||
|
rpc GetUserByEmail(GetUserByEmailRequest) returns(GetUserResponse);
|
||||||
|
rpc ListUser(ListUserRequest) returns(ListUserResponse);
|
||||||
|
|
||||||
|
rpc CreateChat(CreateChatRequest) returns(CreateChatResponse);
|
||||||
|
rpc UpdateChat(UpdateChatRequest) returns(UpdateChatResponse);
|
||||||
|
rpc GetChat(GetChatRequest) returns(GetChatResponse);
|
||||||
|
rpc ListChat(ListChatRequest) returns(ListChatResponse);
|
||||||
|
|
||||||
|
rpc CreateMessage(CreateMessageRequest) returns(CreateMessageResponse);
|
||||||
|
rpc UpdateMessage(UpdateMessageRequest) returns(UpdateMessageResponse);
|
||||||
|
rpc GetMessage(GetMessageRequest) returns(GetMessageResponse);
|
||||||
|
rpc ListMessage(ListMessageRequest) returns(ListMessageResponse);
|
||||||
|
|
||||||
|
rpc GetVersion(google.protobuf.Empty) returns(GetVersionResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User
|
||||||
|
message CreateUserRequest {
|
||||||
|
string email = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateUserResponse {
|
||||||
|
User data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateUserRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
optional string token = 2;
|
||||||
|
optional string description = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateUserResponse {
|
||||||
|
User data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserByTokenRequest {
|
||||||
|
string token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserByEmailRequest {
|
||||||
|
string email = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserResponse {
|
||||||
|
User data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListUserRequest {
|
||||||
|
int32 page = 1;
|
||||||
|
repeated int32 user_ids = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListUserResponse {
|
||||||
|
repeated User data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message User {
|
||||||
|
int32 id = 1;
|
||||||
|
optional string token = 2;
|
||||||
|
string email = 3;
|
||||||
|
optional string description = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserForChatResponse {
|
||||||
|
int32 id = 1;
|
||||||
|
optional string token = 2;
|
||||||
|
string email = 3;
|
||||||
|
optional string description = 4;
|
||||||
|
optional bool is_admin = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User end
|
||||||
|
|
||||||
|
// Chat
|
||||||
|
message CreateChatRequest {
|
||||||
|
repeated UserForChat users = 1;
|
||||||
|
// int32 type_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserForChat {
|
||||||
|
int32 user_id = 1;
|
||||||
|
optional bool is_admin = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateChatResponse {
|
||||||
|
Chat data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateChatRequest {
|
||||||
|
string id = 1;
|
||||||
|
repeated UserForChat users = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateChatResponse {
|
||||||
|
Chat data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetChatRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetChatResponse {
|
||||||
|
Chat data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListChatRequest {
|
||||||
|
int32 page = 1;
|
||||||
|
repeated int32 user_ids = 2;
|
||||||
|
repeated string chat_ids = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListChatResponse {
|
||||||
|
repeated Chat data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Chat {
|
||||||
|
string id = 1;
|
||||||
|
int32 type_id = 3;
|
||||||
|
repeated UserForChatResponse users = 5;
|
||||||
|
}
|
||||||
|
// Chat end
|
||||||
|
|
||||||
|
// Message
|
||||||
|
message CreateMessageRequest {
|
||||||
|
string chat_id = 2;
|
||||||
|
int32 user_id = 3;
|
||||||
|
optional string text = 4;
|
||||||
|
optional string image = 5;
|
||||||
|
optional string video = 6;
|
||||||
|
optional string file = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateMessageResponse {
|
||||||
|
Message data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateMessageRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
string chat_id = 2;
|
||||||
|
optional string text = 3;
|
||||||
|
optional string image = 4;
|
||||||
|
optional string video = 5;
|
||||||
|
optional string file = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateMessageResponse {
|
||||||
|
Message data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetMessageRequest {
|
||||||
|
int32 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetMessageResponse {
|
||||||
|
Message data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListMessageRequest {
|
||||||
|
int32 page = 1;
|
||||||
|
string chat_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListMessageResponse {
|
||||||
|
repeated Message data = 1;
|
||||||
|
}
|
||||||
|
message Message {
|
||||||
|
int32 id = 1;
|
||||||
|
int32 user_id = 2;
|
||||||
|
optional string text = 3;
|
||||||
|
optional string image = 4;
|
||||||
|
optional string video = 5;
|
||||||
|
optional string file = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message end
|
||||||
|
|
||||||
|
// Version
|
||||||
|
message GetVersionResponse {
|
||||||
|
Version data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Version {
|
||||||
|
int32 id = 1;
|
||||||
|
string version = 2;
|
||||||
|
}
|
||||||
|
// Version end
|
||||||
7
req.http
Normal file
7
req.http
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
### Send POST request with json body
|
||||||
|
POST http://localhost:3000/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"login": "foobar"
|
||||||
|
}
|
||||||
10
src/config.ts
Normal file
10
src/config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const EXPIRE = 30 * 60 // 30min
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
accessSecret: process.env.JWT_SECRET ?? 'JWTAccessSecret',
|
||||||
|
refreshSecret: process.env.REFRESH_SECRET ?? 'JWTRefreshSecret',
|
||||||
|
accessTokenExpiry: `${EXPIRE}s`,
|
||||||
|
cookieExpiry: EXPIRE,
|
||||||
|
refreshTokenExpiry: '7d',
|
||||||
|
port: parseInt(process.env.PORT || '3000'),
|
||||||
|
}
|
||||||
9
src/constants.ts
Normal file
9
src/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export enum HttpStatusCodes {
|
||||||
|
OK = 200,
|
||||||
|
CREATED = 201,
|
||||||
|
BAD_REQUEST = 400,
|
||||||
|
UNAUTHORIZED = 401,
|
||||||
|
FORBIDDEN = 403,
|
||||||
|
NOT_FOUND = 404,
|
||||||
|
INTERNAL_SERVER_ERROR = 500,
|
||||||
|
}
|
||||||
95
src/grpc/client.ts
Normal file
95
src/grpc/client.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import protoLoader from '@grpc/proto-loader'
|
||||||
|
import grpc from '@grpc/grpc-js'
|
||||||
|
|
||||||
|
interface Version {
|
||||||
|
id: number
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VersionDto {}
|
||||||
|
|
||||||
|
interface GetUserByEmail {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number
|
||||||
|
email: string
|
||||||
|
token?: string
|
||||||
|
description?: string
|
||||||
|
is_admin?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListChatDto {
|
||||||
|
page: number
|
||||||
|
user_ids?: number[]
|
||||||
|
chat_ids?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Chat {
|
||||||
|
id: string
|
||||||
|
type_id: number
|
||||||
|
users: User[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Response<T> {
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Services {
|
||||||
|
getVersion = 'getVersion',
|
||||||
|
createUser = 'createUser',
|
||||||
|
getUserByEmail = 'getUserByEmail',
|
||||||
|
listUser = 'listUser',
|
||||||
|
listChat = 'listChat',
|
||||||
|
}
|
||||||
|
|
||||||
|
class GrpcClient {
|
||||||
|
messageClient: any
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
console.log('Grpc Client init')
|
||||||
|
|
||||||
|
const PROTO_PATH = path.resolve('./proto/message.proto')
|
||||||
|
console.log('PROTO_PATH', PROTO_PATH)
|
||||||
|
|
||||||
|
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
||||||
|
keepCase: true,
|
||||||
|
longs: String,
|
||||||
|
enums: String,
|
||||||
|
defaults: true,
|
||||||
|
oneofs: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition)
|
||||||
|
const messageProto = protoDescriptor.message as any
|
||||||
|
|
||||||
|
this.messageClient = new messageProto.MessageService('localhost:8070', grpc.credentials.createInsecure())
|
||||||
|
}
|
||||||
|
|
||||||
|
private toPromise<T, R>(client: any, methodName: Services) {
|
||||||
|
return (request: T): Promise<Response<R>> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client[methodName](request, (error: Error, response: Response<R>) => {
|
||||||
|
if (error) reject(error.message)
|
||||||
|
else resolve(response)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVersion(dto: VersionDto) {
|
||||||
|
return this.toPromise<VersionDto, Version>(this.messageClient, Services.getVersion)(dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserByEmail(dto: GetUserByEmail) {
|
||||||
|
return this.toPromise<GetUserByEmail, User>(this.messageClient, Services.getUserByEmail)(dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
getChatsByUser(dto: ListChatDto) {
|
||||||
|
return this.toPromise<ListChatDto, Chat[]>(this.messageClient, Services.listChat)(dto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const grpcClient = new GrpcClient()
|
||||||
64
src/handles.ts
Normal file
64
src/handles.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { HttpStatusCodes } from './constants.ts'
|
||||||
|
import type { LoginDto, WebSocketData } from './types/types.ts'
|
||||||
|
import { createAccessToken, verifyAccessToken } from './utils/jwt.ts'
|
||||||
|
import { grpcClient } from './grpc/client.ts'
|
||||||
|
import { config } from './config.ts'
|
||||||
|
|
||||||
|
export async function loginRequest(req: Request) {
|
||||||
|
try {
|
||||||
|
const body: LoginDto = await req.json()
|
||||||
|
|
||||||
|
const versionResponse = await grpcClient.getVersion({})
|
||||||
|
console.log(versionResponse.data)
|
||||||
|
|
||||||
|
const { email } = body
|
||||||
|
if (!email) return Response.json({ message: 'email required' }, { status: HttpStatusCodes.BAD_REQUEST })
|
||||||
|
|
||||||
|
const userResponse = await grpcClient.getUserByEmail({ email: 'vadim.olonin@gmail.com' })
|
||||||
|
const user = userResponse.data
|
||||||
|
if (!user) return Response.json({ message: 'Invalid email or password' }, { status: HttpStatusCodes.NOT_FOUND })
|
||||||
|
|
||||||
|
const accessToken = await createAccessToken(user.id, user.email)
|
||||||
|
|
||||||
|
const expires = new Date(Date.now() + config.cookieExpiry * 1000)
|
||||||
|
const sessionCookie = new Bun.Cookie('token', accessToken.token, {
|
||||||
|
path: '/',
|
||||||
|
expires: expires,
|
||||||
|
// maxAge: config.cookieExpiry,
|
||||||
|
httpOnly: true,
|
||||||
|
// secure: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
accessToken: accessToken.token,
|
||||||
|
tokenType: 'Bearer',
|
||||||
|
expires: expires,
|
||||||
|
},
|
||||||
|
{ status: HttpStatusCodes.CREATED, headers: { 'Set-Cookie': sessionCookie.toString() } },
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log({ error })
|
||||||
|
return Response.json({ message: 'Login failed' }, { status: HttpStatusCodes.BAD_REQUEST })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function wsRequest(req: Request, server: Bun.Server<WebSocketData>) {
|
||||||
|
try {
|
||||||
|
const cookieHeader = req.headers.get('cookie') ?? ''
|
||||||
|
const cookies = new Bun.CookieMap(cookieHeader)
|
||||||
|
const token = cookies.get('token')
|
||||||
|
if (!token) return Response.json({ message: 'Invalid token' }, { status: HttpStatusCodes.NOT_FOUND })
|
||||||
|
|
||||||
|
const payload = await verifyAccessToken(token)
|
||||||
|
|
||||||
|
const success = server.upgrade(req, { data: { userId: +payload.sub } })
|
||||||
|
if (success) return undefined
|
||||||
|
|
||||||
|
return new Response('Upgrade failed', { status: HttpStatusCodes.INTERNAL_SERVER_ERROR })
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return new Response('Upgrade failed', { status: HttpStatusCodes.INTERNAL_SERVER_ERROR })
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/index.ts
Normal file
80
src/index.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { parseJson } from './utils/utils.ts'
|
||||||
|
import { HttpStatusCodes } from './constants.ts'
|
||||||
|
import type { WebSocketData } from './types/types.ts'
|
||||||
|
import { loginRequest, wsRequest } from './handles.ts'
|
||||||
|
import { grpcClient } from './grpc/client.ts'
|
||||||
|
|
||||||
|
const GROUP = 'group'
|
||||||
|
const PORT = 3000
|
||||||
|
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: PORT,
|
||||||
|
|
||||||
|
async fetch(req, server) {
|
||||||
|
const url = new URL(req.url)
|
||||||
|
const pathname = url.pathname
|
||||||
|
const method = req.method
|
||||||
|
|
||||||
|
if (pathname === '/login' && method === 'POST') return loginRequest(req)
|
||||||
|
if (pathname === '/ws') return wsRequest(req, server)
|
||||||
|
|
||||||
|
return new Response('Not found', { status: HttpStatusCodes.NOT_FOUND })
|
||||||
|
},
|
||||||
|
websocket: {
|
||||||
|
data: {} as WebSocketData,
|
||||||
|
async open(ws) {
|
||||||
|
try {
|
||||||
|
const ipAddr = ws.remoteAddress
|
||||||
|
console.log('ipAddr', ipAddr)
|
||||||
|
|
||||||
|
const chatResponse = await grpcClient.getChatsByUser({ page: 0, user_ids: [1] })
|
||||||
|
chatResponse.data.forEach((el) => ws.subscribe(el.id))
|
||||||
|
|
||||||
|
console.log('chats', chatResponse.data)
|
||||||
|
console.log('subscriptions', ws.subscriptions)
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({ type: 'chats', ...chatResponse }))
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
ws.close(1011, 'error')
|
||||||
|
}
|
||||||
|
|
||||||
|
// server.publish(GROUP, JSON.stringify({
|
||||||
|
// id: Bun.randomUUIDv7(),
|
||||||
|
// createdAt: new Date().toISOString(),
|
||||||
|
// username: ws.data.username,
|
||||||
|
// text: 'connected'
|
||||||
|
// }))
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
console.log('Websocket message', message)
|
||||||
|
// const result = ws.send(message);
|
||||||
|
|
||||||
|
// if (typeof message === 'string') {
|
||||||
|
// const json = parseJson(message)
|
||||||
|
// server.publish(
|
||||||
|
// GROUP,
|
||||||
|
// JSON.stringify({
|
||||||
|
// id: Bun.randomUUIDv7(),
|
||||||
|
// createdAt: new Date().toISOString(),
|
||||||
|
// username: ws.data.username,
|
||||||
|
// text: json.text,
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ws.send(response.arrayBuffer()); // ArrayBuffer
|
||||||
|
// ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView
|
||||||
|
},
|
||||||
|
close(ws, code, message) {
|
||||||
|
console.log('close', ws.data)
|
||||||
|
// ws.unsubscribe(GROUP)
|
||||||
|
// server.publish(GROUP, ws.data.username + ' has left')
|
||||||
|
},
|
||||||
|
drain(ws) {
|
||||||
|
console.log('Websocket drain', ws.data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Listening on ${server.hostname}:${server.port}`)
|
||||||
25
src/types/types.ts
Normal file
25
src/types/types.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export interface WebSocketData {
|
||||||
|
chatId?: string
|
||||||
|
token?: string
|
||||||
|
userId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginDto = { email: string }
|
||||||
|
|
||||||
|
interface ChatData {
|
||||||
|
type: 'chat'
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
type: 'user'
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageData {
|
||||||
|
type: 'message'
|
||||||
|
id: number
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WsData = ChatData | UserData | MessageData
|
||||||
77
src/utils/jwt.ts
Normal file
77
src/utils/jwt.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { type JWTPayload, jwtVerify, SignJWT } from 'jose'
|
||||||
|
import { config } from '../config.ts'
|
||||||
|
import { generateUUID } from './utils.ts'
|
||||||
|
|
||||||
|
export interface TokenPayload extends JWTPayload {
|
||||||
|
sub: string // User ID (subject claim)
|
||||||
|
email: string // User email for convenience
|
||||||
|
type: 'access' | 'refresh' // Token type for validation
|
||||||
|
jti: string // Unique token ID for revocation
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessSecret = new TextEncoder().encode(config.accessSecret)
|
||||||
|
const refreshSecret = new TextEncoder().encode(config.refreshSecret)
|
||||||
|
|
||||||
|
export async function createAccessToken(userId: number, email: string) {
|
||||||
|
const tokenId = generateUUID()
|
||||||
|
|
||||||
|
const token = await new SignJWT({
|
||||||
|
sub: userId.toString(),
|
||||||
|
email: email,
|
||||||
|
type: 'access',
|
||||||
|
jti: tokenId,
|
||||||
|
})
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime(config.accessTokenExpiry)
|
||||||
|
.setIssuer('bun-auth-service')
|
||||||
|
.setAudience('bun-api')
|
||||||
|
.sign(accessSecret)
|
||||||
|
|
||||||
|
return { tokenId, token }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRefreshToken(userId: string, email: string): Promise<{ token: string; tokenId: string }> {
|
||||||
|
const tokenId = generateUUID()
|
||||||
|
|
||||||
|
const token = await new SignJWT({
|
||||||
|
sub: userId,
|
||||||
|
email: email,
|
||||||
|
type: 'refresh',
|
||||||
|
jti: tokenId,
|
||||||
|
})
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime(config.refreshTokenExpiry) // Longer expiry
|
||||||
|
.setIssuer('bun-auth-service')
|
||||||
|
.setAudience('bun-api')
|
||||||
|
.sign(refreshSecret)
|
||||||
|
|
||||||
|
return { token, tokenId }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyAccessToken(token: string) {
|
||||||
|
const { payload } = await jwtVerify(token, accessSecret, {
|
||||||
|
issuer: 'bun-auth-service',
|
||||||
|
audience: 'bun-api',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (payload.type !== 'access') {
|
||||||
|
throw new Error('Invalid token type')
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload as TokenPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyRefreshToken(token: string) {
|
||||||
|
const { payload } = await jwtVerify(token, refreshSecret, {
|
||||||
|
issuer: 'bun-auth-service',
|
||||||
|
audience: 'bun-api',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (payload.type !== 'refresh') {
|
||||||
|
throw new Error('Invalid token type')
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload as TokenPayload
|
||||||
|
}
|
||||||
18
src/utils/utils.ts
Normal file
18
src/utils/utils.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export function parseJson(str: string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str)
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function json(data: object, status: number = 200): Response {
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
status,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateUUID() {
|
||||||
|
return Bun.randomUUIDv7()
|
||||||
|
}
|
||||||
32
tsconfig.json
Normal file
32
tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user