diff --git a/examples/user-authentication/backend/.gitignore b/examples/user-authentication/backend/.gitignore new file mode 100644 index 0000000..d84f0da --- /dev/null +++ b/examples/user-authentication/backend/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.DS_STORE \ No newline at end of file diff --git a/examples/user-authentication/backend/.vscode/launch.json b/examples/user-authentication/backend/.vscode/launch.json new file mode 100644 index 0000000..9ba4218 --- /dev/null +++ b/examples/user-authentication/backend/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "mocha current file", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "${file}" + ], + "internalConsoleOptions": "openOnSessionStart", + "cwd": "${workspaceFolder}" + }, + { + "type": "node", + "request": "launch", + "name": "ts-node current file", + "protocol": "inspector", + "args": [ + "${relativeFile}" + ], + "cwd": "${workspaceRoot}", + "runtimeArgs": [ + "-r", + "ts-node/register" + ], + "internalConsoleOptions": "openOnSessionStart" + } + ] +} \ No newline at end of file diff --git a/examples/user-authentication/backend/.vscode/settings.json b/examples/user-authentication/backend/.vscode/settings.json new file mode 100644 index 0000000..00ad71f --- /dev/null +++ b/examples/user-authentication/backend/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/examples/user-authentication/backend/README.md b/examples/user-authentication/backend/README.md new file mode 100644 index 0000000..7492c22 --- /dev/null +++ b/examples/user-authentication/backend/README.md @@ -0,0 +1,35 @@ +# TSRPC Server + +## Run +### Local Dev Server +``` +npm run dev +``` + + + +### Build +``` +npm run build +``` + +--- + +## Files +### Generate ServiceProto +``` +npm run proto +``` + +### Generate API templates +``` +npm run api +``` + +### Sync shared code to client + +``` +npm run sync +``` + +> If you chose symlink when using `create-tsrpc-app`, it would re-create the symlink instead of copy files. diff --git a/examples/user-authentication/backend/package.json b/examples/user-authentication/backend/package.json new file mode 100644 index 0000000..a225f57 --- /dev/null +++ b/examples/user-authentication/backend/package.json @@ -0,0 +1,25 @@ +{ + "name": "user-authentication-backend", + "version": "0.1.0", + "main": "index.js", + "private": true, + "scripts": { + "proto": "tsrpc proto -i src/shared/protocols -o src/shared/protocols/serviceProto.ts", + "sync": "tsrpc sync --from src/shared --to ../frontend/src/shared", + "api": "tsrpc api -i src/shared/protocols/serviceProto.ts -o src/api", + "dev": "onchange \"src/**/*.ts\" -i -k -- ts-node \"src/index.ts\"", + "build": "tsrpc build" + }, + "devDependencies": { + "@types/node": "^15.12.2", + "@types/uuid": "^8.3.0", + "onchange": "^7.1.0", + "ts-node": "^9.1.1", + "tsrpc-cli": "^2.0.1-dev.12", + "typescript": "^4.3.2" + }, + "dependencies": { + "tsrpc": "^3.0.0-dev.20", + "uuid": "^8.3.2" + } +} diff --git a/examples/user-authentication/backend/src/api/action/ApiAdminAction.ts b/examples/user-authentication/backend/src/api/action/ApiAdminAction.ts new file mode 100644 index 0000000..5a068bc --- /dev/null +++ b/examples/user-authentication/backend/src/api/action/ApiAdminAction.ts @@ -0,0 +1,8 @@ +import { ApiCall } from "tsrpc"; +import { ReqAdminAction, ResAdminAction } from "../../shared/protocols/action/PtlAdminAction"; + +export async function ApiAdminAction(call: ApiCall) { + call.succ({ + result: 'Success' + }) +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/api/action/ApiGuestAction.ts b/examples/user-authentication/backend/src/api/action/ApiGuestAction.ts new file mode 100644 index 0000000..2461787 --- /dev/null +++ b/examples/user-authentication/backend/src/api/action/ApiGuestAction.ts @@ -0,0 +1,8 @@ +import { ApiCall } from "tsrpc"; +import { ReqGuestAction, ResGuestAction } from "../../shared/protocols/action/PtlGuestAction"; + +export async function ApiGuestAction(call: ApiCall) { + call.succ({ + result: 'Success' + }) +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/api/action/ApiNormalAction.ts b/examples/user-authentication/backend/src/api/action/ApiNormalAction.ts new file mode 100644 index 0000000..7506113 --- /dev/null +++ b/examples/user-authentication/backend/src/api/action/ApiNormalAction.ts @@ -0,0 +1,8 @@ +import { ApiCall } from "tsrpc"; +import { ReqNormalAction, ResNormalAction } from "../../shared/protocols/action/PtlNormalAction"; + +export async function ApiNormalAction(call: ApiCall) { + call.succ({ + result: 'Success' + }) +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/api/user/ApiLogin.ts b/examples/user-authentication/backend/src/api/user/ApiLogin.ts new file mode 100644 index 0000000..853ff2b --- /dev/null +++ b/examples/user-authentication/backend/src/api/user/ApiLogin.ts @@ -0,0 +1,17 @@ +import { ApiCall } from "tsrpc"; +import { UserUtil } from "../../models/UserUtil"; +import { ReqLogin, ResLogin } from "../../shared/protocols/user/PtlLogin"; + +export async function ApiLogin(call: ApiCall) { + let user = UserUtil.users.find(v => v.username === call.req.username && v.password === call.req.password); + if (!user) { + call.error('Error username or password'); + return; + } + + let sso = await UserUtil.createSsoToken(user.uid); + + call.succ({ + __ssoToken: sso + }) +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/api/user/ApiLogout.ts b/examples/user-authentication/backend/src/api/user/ApiLogout.ts new file mode 100644 index 0000000..cc2010b --- /dev/null +++ b/examples/user-authentication/backend/src/api/user/ApiLogout.ts @@ -0,0 +1,10 @@ +import { ApiCall } from "tsrpc"; +import { UserUtil } from "../../models/UserUtil"; +import { ReqLogout, ResLogout } from "../../shared/protocols/user/PtlLogout"; + +export async function ApiLogout(call: ApiCall) { + call.req.__ssoToken && UserUtil.destroySsoToken(call.req.__ssoToken); + call.succ({ + __ssoToken: '' + }); +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/index.ts b/examples/user-authentication/backend/src/index.ts new file mode 100644 index 0000000..8e29015 --- /dev/null +++ b/examples/user-authentication/backend/src/index.ts @@ -0,0 +1,31 @@ +import * as path from "path"; +import { HttpServer } from "tsrpc"; +import { enableAuthentication } from "./models/enableAuthentication"; +import { parseCurrentUser } from "./models/parseCurrentUser"; +import { serviceProto } from "./shared/protocols/serviceProto"; + +// Create the Server +const server = new HttpServer(serviceProto, { + port: 3000, + cors: '*' +}); + +parseCurrentUser(server); +enableAuthentication(server); + +// Entry function +async function main() { + // Auto implement APIs + await server.autoImplementApi(path.resolve(__dirname, 'api')); + + // TODO + // Prepare something... (e.g. connect the db) + + await server.start(); +}; + +main().catch(e => { + // Exit if any error during the startup + server.logger.error(e); + process.exit(-1); +}); \ No newline at end of file diff --git a/examples/user-authentication/backend/src/models/CurrentUser.ts b/examples/user-authentication/backend/src/models/CurrentUser.ts new file mode 100644 index 0000000..283f7b1 --- /dev/null +++ b/examples/user-authentication/backend/src/models/CurrentUser.ts @@ -0,0 +1,5 @@ +export interface CurrentUser { + uid: number, + username: string, + roles: string[] +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/models/UserUtil.ts b/examples/user-authentication/backend/src/models/UserUtil.ts new file mode 100644 index 0000000..6732ada --- /dev/null +++ b/examples/user-authentication/backend/src/models/UserUtil.ts @@ -0,0 +1,76 @@ +import * as uuid from "uuid"; +import { CurrentUser } from "./CurrentUser"; + +const SSO_VALID_TIME = 86400000 * 7; + +export class UserUtil { + + // Store data in memory for test + // You can store data into database + static users: { + uid: number, + username: string, + password: string, + roles: string[] + }[] = [ + { + uid: 1, + username: 'Normal', + password: '123456', + roles: ['Normal'] + }, + { + uid: 2, + username: 'Admin', + password: '123456', + roles: ['Admin'] + } + ]; + + static ssoTokenInfo: { + [token: string]: { expiredTime: number, uid: number } + } = {}; + + static async createSsoToken(uid: number): Promise { + let token = uuid.v1(); + // Expired after some time without any action + let expiredTime = Date.now() + SSO_VALID_TIME; + + this.ssoTokenInfo[token] = { + uid: uid, + expiredTime: expiredTime + }; + + return token; + } + + static async destroySsoToken(ssoToken: string): Promise { + delete this.ssoTokenInfo[ssoToken]; + } + + static async parseSSO(ssoToken: string): Promise { + let info = this.ssoTokenInfo[ssoToken]; + // Token not exists or expired + if (!info || info.expiredTime < Date.now()) { + return undefined; + } + + // Parse User + let user = this.users.find(v => v.uid === info.uid); + if (!user) { + return undefined; + } + + // Extend expired time + info.expiredTime = Date.now() + SSO_VALID_TIME; + + // Return parsed CurrentUser + return { + uid: user.uid, + username: user.username, + roles: user.roles + } + + } + +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/models/enableAuthentication.ts b/examples/user-authentication/backend/src/models/enableAuthentication.ts new file mode 100644 index 0000000..01643bf --- /dev/null +++ b/examples/user-authentication/backend/src/models/enableAuthentication.ts @@ -0,0 +1,22 @@ +import { HttpServer } from "tsrpc"; +import { BaseConf } from "../shared/protocols/base"; + +export function enableAuthentication(server: HttpServer) { + server.flows.preApiCallFlow.push(call => { + let conf: BaseConf | undefined = call.service.conf; + + // NeedLogin + if (conf?.needLogin && !call.currentUser) { + call.error('You need login before do this', { code: 'NEED_LOGIN' }); + return undefined; + } + + // NeedRoles + if (conf?.needRoles?.length && !call.currentUser?.roles.some(v => conf!.needRoles!.indexOf(v) > -1)) { + call.error('You do NOT have authority to do this', { code: 'NO_AUTHORITY' }); + return undefined; + } + + return call; + }) +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/models/parseCurrentUser.ts b/examples/user-authentication/backend/src/models/parseCurrentUser.ts new file mode 100644 index 0000000..b5f561e --- /dev/null +++ b/examples/user-authentication/backend/src/models/parseCurrentUser.ts @@ -0,0 +1,21 @@ +import { HttpServer } from "tsrpc"; +import { BaseRequest } from "../shared/protocols/base"; +import { CurrentUser } from "./CurrentUser"; +import { UserUtil } from "./UserUtil"; + +export function parseCurrentUser(server: HttpServer) { + // Auto parse call.currentUser + server.flows.preApiCallFlow.push(async call => { + let req = call.req as BaseRequest; + if (req.__ssoToken) { + call.currentUser = await UserUtil.parseSSO(req.__ssoToken); + } + return call; + }) +} + +declare module 'tsrpc' { + export interface ApiCall { + currentUser?: CurrentUser; + } +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/shared/protocols/action/PtlAdminAction.ts b/examples/user-authentication/backend/src/shared/protocols/action/PtlAdminAction.ts new file mode 100644 index 0000000..77a9aea --- /dev/null +++ b/examples/user-authentication/backend/src/shared/protocols/action/PtlAdminAction.ts @@ -0,0 +1,14 @@ +import { BaseRequest, BaseResponse, BaseConf } from '../base' + +export interface ReqAdminAction extends BaseRequest { + +} + +export interface ResAdminAction extends BaseResponse { + result: string +} + +export const conf: BaseConf = { + needLogin: true, + needRoles: ['Admin'] +}; \ No newline at end of file diff --git a/examples/user-authentication/backend/src/shared/protocols/action/PtlGuestAction.ts b/examples/user-authentication/backend/src/shared/protocols/action/PtlGuestAction.ts new file mode 100644 index 0000000..7653d77 --- /dev/null +++ b/examples/user-authentication/backend/src/shared/protocols/action/PtlGuestAction.ts @@ -0,0 +1,13 @@ +import { BaseConf, BaseRequest, BaseResponse } from '../base'; + +export interface ReqGuestAction extends BaseRequest { + +} + +export interface ResGuestAction extends BaseResponse { + result: string +} + +export const conf: BaseConf = { + needLogin: false +}; \ No newline at end of file diff --git a/examples/user-authentication/backend/src/shared/protocols/action/PtlNormalAction.ts b/examples/user-authentication/backend/src/shared/protocols/action/PtlNormalAction.ts new file mode 100644 index 0000000..f7d54b1 --- /dev/null +++ b/examples/user-authentication/backend/src/shared/protocols/action/PtlNormalAction.ts @@ -0,0 +1,13 @@ +import { BaseConf, BaseRequest, BaseResponse } from '../base'; + +export interface ReqNormalAction extends BaseRequest { + +} + +export interface ResNormalAction extends BaseResponse { + result: string +} + +export const conf: BaseConf = { + needLogin: true +}; \ No newline at end of file diff --git a/examples/user-authentication/backend/src/shared/protocols/base.ts b/examples/user-authentication/backend/src/shared/protocols/base.ts new file mode 100644 index 0000000..2b261fd --- /dev/null +++ b/examples/user-authentication/backend/src/shared/protocols/base.ts @@ -0,0 +1,13 @@ +export interface BaseRequest { + __ssoToken?: string; +} + +export interface BaseResponse { + // Init or refresh sso token + __ssoToken?: string; +} + +export interface BaseConf { + needLogin?: boolean, + needRoles?: string[] +} \ No newline at end of file diff --git a/examples/user-authentication/backend/src/shared/protocols/serviceProto.ts b/examples/user-authentication/backend/src/shared/protocols/serviceProto.ts new file mode 100644 index 0000000..f3d0c53 --- /dev/null +++ b/examples/user-authentication/backend/src/shared/protocols/serviceProto.ts @@ -0,0 +1,279 @@ +import { ServiceProto } from 'tsrpc-proto'; +import { ReqAdminAction, ResAdminAction } from './action/PtlAdminAction'; +import { ReqGuestAction, ResGuestAction } from './action/PtlGuestAction'; +import { ReqNormalAction, ResNormalAction } from './action/PtlNormalAction'; +import { ReqLogin, ResLogin } from './user/PtlLogin'; +import { ReqLogout, ResLogout } from './user/PtlLogout'; + +export interface ServiceType { + api: { + "action/AdminAction": { + req: ReqAdminAction, + res: ResAdminAction + }, + "action/GuestAction": { + req: ReqGuestAction, + res: ResGuestAction + }, + "action/NormalAction": { + req: ReqNormalAction, + res: ResNormalAction + }, + "user/Login": { + req: ReqLogin, + res: ResLogin + }, + "user/Logout": { + req: ReqLogout, + res: ResLogout + } + }, + msg: { + + } +} + +export const serviceProto: ServiceProto = { + "version": 2, + "services": [ + { + "id": 0, + "name": "action/AdminAction", + "type": "api", + "conf": { + "needLogin": true, + "needRoles": [ + "Admin" + ] + } + }, + { + "id": 1, + "name": "action/GuestAction", + "type": "api", + "conf": { + "needLogin": false + } + }, + { + "id": 2, + "name": "action/NormalAction", + "type": "api", + "conf": { + "needLogin": true + } + }, + { + "id": 3, + "name": "user/Login", + "type": "api", + "conf": {} + }, + { + "id": 4, + "name": "user/Logout", + "type": "api", + "conf": {} + } + ], + "types": { + "action/PtlAdminAction/ReqAdminAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ] + }, + "base/BaseRequest": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "__ssoToken", + "type": { + "type": "String" + }, + "optional": true + } + ] + }, + "action/PtlAdminAction/ResAdminAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ], + "properties": [ + { + "id": 0, + "name": "result", + "type": { + "type": "String" + } + } + ] + }, + "base/BaseResponse": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "__ssoToken", + "type": { + "type": "String" + }, + "optional": true + } + ] + }, + "action/PtlGuestAction/ReqGuestAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ] + }, + "action/PtlGuestAction/ResGuestAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ], + "properties": [ + { + "id": 0, + "name": "result", + "type": { + "type": "String" + } + } + ] + }, + "action/PtlNormalAction/ReqNormalAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ] + }, + "action/PtlNormalAction/ResNormalAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ], + "properties": [ + { + "id": 0, + "name": "result", + "type": { + "type": "String" + } + } + ] + }, + "user/PtlLogin/ReqLogin": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ], + "properties": [ + { + "id": 0, + "name": "username", + "type": { + "type": "String" + } + }, + { + "id": 1, + "name": "password", + "type": { + "type": "String" + } + } + ] + }, + "user/PtlLogin/ResLogin": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ], + "properties": [ + { + "id": 0, + "name": "__ssoToken", + "type": { + "type": "String" + } + } + ] + }, + "user/PtlLogout/ReqLogout": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ] + }, + "user/PtlLogout/ResLogout": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ] + } + } +}; \ No newline at end of file diff --git a/examples/user-authentication/backend/src/shared/protocols/user/PtlLogin.ts b/examples/user-authentication/backend/src/shared/protocols/user/PtlLogin.ts new file mode 100644 index 0000000..ddcd494 --- /dev/null +++ b/examples/user-authentication/backend/src/shared/protocols/user/PtlLogin.ts @@ -0,0 +1,14 @@ +import { BaseConf, BaseRequest, BaseResponse } from '../base'; + +export interface ReqLogin extends BaseRequest { + username: string, + password: string +} + +export interface ResLogin extends BaseResponse { + __ssoToken: string; +} + +export const conf: BaseConf = { + +}; \ No newline at end of file diff --git a/examples/user-authentication/backend/src/shared/protocols/user/PtlLogout.ts b/examples/user-authentication/backend/src/shared/protocols/user/PtlLogout.ts new file mode 100644 index 0000000..c3b112b --- /dev/null +++ b/examples/user-authentication/backend/src/shared/protocols/user/PtlLogout.ts @@ -0,0 +1,13 @@ +import { BaseRequest, BaseResponse, BaseConf } from '../base' + +export interface ReqLogout extends BaseRequest { + +} + +export interface ResLogout extends BaseResponse { + +} + +export const conf: BaseConf = { + +}; \ No newline at end of file diff --git a/examples/user-authentication/backend/tsconfig.json b/examples/user-authentication/backend/tsconfig.json new file mode 100644 index 0000000..d18498f --- /dev/null +++ b/examples/user-authentication/backend/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "lib": [ + "es2018" + ], + "module": "commonjs", + "target": "es2018", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node" + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/examples/user-authentication/frontend/.gitignore b/examples/user-authentication/frontend/.gitignore new file mode 100644 index 0000000..ae16b5a --- /dev/null +++ b/examples/user-authentication/frontend/.gitignore @@ -0,0 +1,3 @@ +.DS_STORE +node_modules +dist \ No newline at end of file diff --git a/examples/user-authentication/frontend/package.json b/examples/user-authentication/frontend/package.json new file mode 100644 index 0000000..f713f1a --- /dev/null +++ b/examples/user-authentication/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "user-authentication-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "webpack serve --mode=development --open", + "build": "webpack --mode=production" + }, + "devDependencies": { + "copy-webpack-plugin": "^9.0.0", + "html-webpack-plugin": "^5.3.1", + "ts-loader": "^9.2.3", + "typescript": "^4.3.2", + "webpack": "^5.38.1", + "webpack-cli": "^4.7.2", + "webpack-dev-server": "^3.11.2" + }, + "dependencies": { + "tsrpc-browser": "^3.0.0-dev.16" + } +} diff --git a/examples/user-authentication/frontend/public/favicon.ico b/examples/user-authentication/frontend/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/examples/user-authentication/frontend/public/index.css b/examples/user-authentication/frontend/public/index.css new file mode 100644 index 0000000..e0f3c74 --- /dev/null +++ b/examples/user-authentication/frontend/public/index.css @@ -0,0 +1,99 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + padding: 20px; +} + +h1, +h2 { + text-align: center; + margin-bottom: 20px; +} + +.status { + width: 1000px; + margin: 20px auto; + text-align: center; + background: #f2f2f2; + padding: 20px; +} + +.status.logined { + background: #2c81b9; + color: #fff; +} + +.row { + display: flex; + background-color: #f2f2f2; + padding: 20px; + margin: 20px auto 0 auto; + width: 1000px; +} + +.row>* { + flex: 1; + margin-right: 20px; + padding: 20px; + background: #d9d9d9; +} + +.row>*:last-child { + margin-right: 0; +} + +label { + display: inline-block; + width: 40%; + line-height: 1.5rem; +} + +input { + display: inline-block; + width: 60%; + padding: 10px; + font-size: 1rem; +} + +p { + margin-bottom: 10px; +} + +button { + font-size: 20px; + padding: 10px 20px; + cursor: pointer; + display: block; + margin: 10px auto; +} + +.action>div { + text-align: center; +} + +.action .hint { + font-size: 14px; + color: #999; +} + +.action h3 { + margin-bottom: 10px; +} + +pre { + background: #333; + border-radius: 5px; + padding: 10px; + color: #fff; + min-height: 80px; + text-align: left; + white-space: pre-wrap; +} + +.return { + display: none; +} \ No newline at end of file diff --git a/examples/user-authentication/frontend/public/index.html b/examples/user-authentication/frontend/public/index.html new file mode 100644 index 0000000..d3adbd3 --- /dev/null +++ b/examples/user-authentication/frontend/public/index.html @@ -0,0 +1,68 @@ + + + + + + + + TSRPC Example + + + + +

Login and Role Authentication

+ +
Not Logined
+ +
+ + + + +
+

Logout

+

+
+
+ +
+
+

Normal Action

+

Need login

+

+
+

Return

+

+            
+
+ +
+

Admin Action

+

Need Admin role

+

+
+

Return

+

+            
+
+ +
+

Guest Action

+

Do NOT need login

+

+
+

Return

+

+            
+
+
+ + + + \ No newline at end of file diff --git a/examples/user-authentication/frontend/src/index.ts b/examples/user-authentication/frontend/src/index.ts new file mode 100644 index 0000000..51c8679 --- /dev/null +++ b/examples/user-authentication/frontend/src/index.ts @@ -0,0 +1,108 @@ +import { HttpClient } from 'tsrpc-browser'; +import { BaseResponse } from './shared/protocols/base'; +import { serviceProto } from './shared/protocols/serviceProto'; + +const $ = document.querySelector.bind(document) as (v: string) => HTMLElement; + +// Create Client +let client = new HttpClient(serviceProto, { + server: 'http://127.0.0.1:3000', + logger: console +}); + +// Flow +client.flows.postApiReturnFlow.push(v => { + if (v.return.isSucc) { + let res = v.return.res as BaseResponse; + if (res.__ssoToken !== undefined) { + localStorage.setItem('SSO_TOKEN', res.__ssoToken); + } + } + else if (v.return.err.code === 'NEED_LOGIN') { + localStorage.removeItem('SSO_TOKEN'); + setStatus(false); + } + return v; +}); +client.flows.preCallApiFlow.push(v => { + let ssoToken = localStorage.getItem('SSO_TOKEN'); + if (ssoToken) { + v.req.__ssoToken = ssoToken; + } + return v; +}) + +// User +$('.login-normal button').onclick = async () => { + let ret = await client.callApi('user/Login', { + username: 'Normal', + password: '123456' + }); + + if (!ret.isSucc) { + alert(ret.err.message); + return; + } + + localStorage.setItem('LoginedRole', 'Normal'); + setStatus(true); + document.querySelectorAll('.return').forEach(v => { v.style.display = 'none' }); +} +$('.login-admin button').onclick = async () => { + let ret = await client.callApi('user/Login', { + username: 'Admin', + password: '123456' + }); + + if (!ret.isSucc) { + alert(ret.err.message); + return; + } + + localStorage.setItem('LoginedRole', 'Admin'); + setStatus(true); + document.querySelectorAll('.return').forEach(v => { v.style.display = 'none' }); +} +$('.logout button').onclick = async () => { + let ret = await client.callApi('user/Logout', {}); + + if (!ret.isSucc) { + alert(ret.err.message); + return; + } + + setStatus(false); + document.querySelectorAll('.return').forEach(v => { v.style.display = 'none' }); +} + +// Actions +$('.action .guest button').onclick = async () => { + let ret = await client.callApi('action/GuestAction', {}); + $('.action .guest pre').innerText = JSON.stringify(ret, null, 2); + $('.action .guest pre').style.background = ret.isSucc ? 'green' : 'darkred'; + $('.action .guest .return').style.display = 'block'; +} +$('.action .normal button').onclick = async () => { + let ret = await client.callApi('action/NormalAction', {}); + $('.action .normal pre').innerText = JSON.stringify(ret, null, 2); + $('.action .normal pre').style.background = ret.isSucc ? 'green' : 'darkred'; + $('.action .normal .return').style.display = 'block'; +} +$('.action .admin button').onclick = async () => { + let ret = await client.callApi('action/AdminAction', {}); + $('.action .admin pre').innerText = JSON.stringify(ret, null, 2); + $('.action .admin pre').style.background = ret.isSucc ? 'green' : 'darkred'; + $('.action .admin .return').style.display = 'block'; +} + +function setStatus(logined: boolean) { + if (logined) { + $('.status').className = 'status logined'; + $('.status').innerText = `Logined as ${localStorage.getItem('LoginedRole')} Role`; + } + else { + $('.status').className = 'status'; + $('.status').innerText = 'Not Logined'; + } +} +setStatus(!!localStorage.getItem('SSO_TOKEN')); \ No newline at end of file diff --git a/examples/user-authentication/frontend/src/shared/protocols/action/PtlAdminAction.ts b/examples/user-authentication/frontend/src/shared/protocols/action/PtlAdminAction.ts new file mode 100644 index 0000000..77a9aea --- /dev/null +++ b/examples/user-authentication/frontend/src/shared/protocols/action/PtlAdminAction.ts @@ -0,0 +1,14 @@ +import { BaseRequest, BaseResponse, BaseConf } from '../base' + +export interface ReqAdminAction extends BaseRequest { + +} + +export interface ResAdminAction extends BaseResponse { + result: string +} + +export const conf: BaseConf = { + needLogin: true, + needRoles: ['Admin'] +}; \ No newline at end of file diff --git a/examples/user-authentication/frontend/src/shared/protocols/action/PtlGuestAction.ts b/examples/user-authentication/frontend/src/shared/protocols/action/PtlGuestAction.ts new file mode 100644 index 0000000..7653d77 --- /dev/null +++ b/examples/user-authentication/frontend/src/shared/protocols/action/PtlGuestAction.ts @@ -0,0 +1,13 @@ +import { BaseConf, BaseRequest, BaseResponse } from '../base'; + +export interface ReqGuestAction extends BaseRequest { + +} + +export interface ResGuestAction extends BaseResponse { + result: string +} + +export const conf: BaseConf = { + needLogin: false +}; \ No newline at end of file diff --git a/examples/user-authentication/frontend/src/shared/protocols/action/PtlNormalAction.ts b/examples/user-authentication/frontend/src/shared/protocols/action/PtlNormalAction.ts new file mode 100644 index 0000000..f7d54b1 --- /dev/null +++ b/examples/user-authentication/frontend/src/shared/protocols/action/PtlNormalAction.ts @@ -0,0 +1,13 @@ +import { BaseConf, BaseRequest, BaseResponse } from '../base'; + +export interface ReqNormalAction extends BaseRequest { + +} + +export interface ResNormalAction extends BaseResponse { + result: string +} + +export const conf: BaseConf = { + needLogin: true +}; \ No newline at end of file diff --git a/examples/user-authentication/frontend/src/shared/protocols/base.ts b/examples/user-authentication/frontend/src/shared/protocols/base.ts new file mode 100644 index 0000000..2b261fd --- /dev/null +++ b/examples/user-authentication/frontend/src/shared/protocols/base.ts @@ -0,0 +1,13 @@ +export interface BaseRequest { + __ssoToken?: string; +} + +export interface BaseResponse { + // Init or refresh sso token + __ssoToken?: string; +} + +export interface BaseConf { + needLogin?: boolean, + needRoles?: string[] +} \ No newline at end of file diff --git a/examples/user-authentication/frontend/src/shared/protocols/serviceProto.ts b/examples/user-authentication/frontend/src/shared/protocols/serviceProto.ts new file mode 100644 index 0000000..f3d0c53 --- /dev/null +++ b/examples/user-authentication/frontend/src/shared/protocols/serviceProto.ts @@ -0,0 +1,279 @@ +import { ServiceProto } from 'tsrpc-proto'; +import { ReqAdminAction, ResAdminAction } from './action/PtlAdminAction'; +import { ReqGuestAction, ResGuestAction } from './action/PtlGuestAction'; +import { ReqNormalAction, ResNormalAction } from './action/PtlNormalAction'; +import { ReqLogin, ResLogin } from './user/PtlLogin'; +import { ReqLogout, ResLogout } from './user/PtlLogout'; + +export interface ServiceType { + api: { + "action/AdminAction": { + req: ReqAdminAction, + res: ResAdminAction + }, + "action/GuestAction": { + req: ReqGuestAction, + res: ResGuestAction + }, + "action/NormalAction": { + req: ReqNormalAction, + res: ResNormalAction + }, + "user/Login": { + req: ReqLogin, + res: ResLogin + }, + "user/Logout": { + req: ReqLogout, + res: ResLogout + } + }, + msg: { + + } +} + +export const serviceProto: ServiceProto = { + "version": 2, + "services": [ + { + "id": 0, + "name": "action/AdminAction", + "type": "api", + "conf": { + "needLogin": true, + "needRoles": [ + "Admin" + ] + } + }, + { + "id": 1, + "name": "action/GuestAction", + "type": "api", + "conf": { + "needLogin": false + } + }, + { + "id": 2, + "name": "action/NormalAction", + "type": "api", + "conf": { + "needLogin": true + } + }, + { + "id": 3, + "name": "user/Login", + "type": "api", + "conf": {} + }, + { + "id": 4, + "name": "user/Logout", + "type": "api", + "conf": {} + } + ], + "types": { + "action/PtlAdminAction/ReqAdminAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ] + }, + "base/BaseRequest": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "__ssoToken", + "type": { + "type": "String" + }, + "optional": true + } + ] + }, + "action/PtlAdminAction/ResAdminAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ], + "properties": [ + { + "id": 0, + "name": "result", + "type": { + "type": "String" + } + } + ] + }, + "base/BaseResponse": { + "type": "Interface", + "properties": [ + { + "id": 0, + "name": "__ssoToken", + "type": { + "type": "String" + }, + "optional": true + } + ] + }, + "action/PtlGuestAction/ReqGuestAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ] + }, + "action/PtlGuestAction/ResGuestAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ], + "properties": [ + { + "id": 0, + "name": "result", + "type": { + "type": "String" + } + } + ] + }, + "action/PtlNormalAction/ReqNormalAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ] + }, + "action/PtlNormalAction/ResNormalAction": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ], + "properties": [ + { + "id": 0, + "name": "result", + "type": { + "type": "String" + } + } + ] + }, + "user/PtlLogin/ReqLogin": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ], + "properties": [ + { + "id": 0, + "name": "username", + "type": { + "type": "String" + } + }, + { + "id": 1, + "name": "password", + "type": { + "type": "String" + } + } + ] + }, + "user/PtlLogin/ResLogin": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ], + "properties": [ + { + "id": 0, + "name": "__ssoToken", + "type": { + "type": "String" + } + } + ] + }, + "user/PtlLogout/ReqLogout": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseRequest" + } + } + ] + }, + "user/PtlLogout/ResLogout": { + "type": "Interface", + "extends": [ + { + "id": 0, + "type": { + "type": "Reference", + "target": "base/BaseResponse" + } + } + ] + } + } +}; \ No newline at end of file diff --git a/examples/user-authentication/frontend/src/shared/protocols/user/PtlLogin.ts b/examples/user-authentication/frontend/src/shared/protocols/user/PtlLogin.ts new file mode 100644 index 0000000..ddcd494 --- /dev/null +++ b/examples/user-authentication/frontend/src/shared/protocols/user/PtlLogin.ts @@ -0,0 +1,14 @@ +import { BaseConf, BaseRequest, BaseResponse } from '../base'; + +export interface ReqLogin extends BaseRequest { + username: string, + password: string +} + +export interface ResLogin extends BaseResponse { + __ssoToken: string; +} + +export const conf: BaseConf = { + +}; \ No newline at end of file diff --git a/examples/user-authentication/frontend/src/shared/protocols/user/PtlLogout.ts b/examples/user-authentication/frontend/src/shared/protocols/user/PtlLogout.ts new file mode 100644 index 0000000..c3b112b --- /dev/null +++ b/examples/user-authentication/frontend/src/shared/protocols/user/PtlLogout.ts @@ -0,0 +1,13 @@ +import { BaseRequest, BaseResponse, BaseConf } from '../base' + +export interface ReqLogout extends BaseRequest { + +} + +export interface ResLogout extends BaseResponse { + +} + +export const conf: BaseConf = { + +}; \ No newline at end of file diff --git a/examples/user-authentication/frontend/tsconfig.json b/examples/user-authentication/frontend/tsconfig.json new file mode 100644 index 0000000..7517bf5 --- /dev/null +++ b/examples/user-authentication/frontend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "es2018" + ], + "module": "esnext", + "target": "es2018", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "outDir": "dist", + "skipLibCheck": true, + "strict": true, + "jsx": "react-jsx", + "sourceMap": true, + "isolatedModules": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/examples/user-authentication/frontend/webpack.config.js b/examples/user-authentication/frontend/webpack.config.js new file mode 100644 index 0000000..062d1a4 --- /dev/null +++ b/examples/user-authentication/frontend/webpack.config.js @@ -0,0 +1,58 @@ +const webpack = require('webpack'); +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +const isProduction = process.argv.indexOf('--mode=production') > -1; + +module.exports = { + entry: './src/index.ts', + output: { + filename: 'bundle.[contenthash].js', + path: path.resolve(__dirname, 'dist'), + clean: true + }, + resolve: { + extensions: ['.ts', '.tsx', '.js'] + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: [{ + loader: 'ts-loader', + options: { + compilerOptions: isProduction ? { + "lib": [ + "dom", + "es2015.promise" + ], + "target": "es5", + } : undefined + } + }], + exclude: /node_modules/ + }, + ] + }, + plugins: [ + // Copy "public" to "dist" + new CopyWebpackPlugin({ + patterns: [{ + from: 'public', + to: '.', + toType: 'dir', + globOptions: { + gitignore: true, + ignore: [path.resolve(__dirname, 'public/index.html').replace(/\\/g, '/')] + }, + noErrorOnMissing: true + }] + }), + // Auto add