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