[WIP] login

main
Johnson C 2 years ago
parent c1e9271609
commit dcc3f30afa

7
.gitignore vendored

@ -11,5 +11,8 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Swagger
docs/
# frontend dist
web/

@ -0,0 +1,23 @@
FROM golang:1.17 as api-builder
WORKDIR /usr/local/bin
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o dashboard .
FROM node:17 as web-builder
WORKDIR /usr/local/bin
RUN cd frontend && npm install && ng build --output-path web
FROM alpine:latest
WORKDIR /usr/local/bin
COPY --from=api-builder /usr/local/bin/dashboard .
COPY --from=web-builder /usr/local/bin/web .
EXPOSE 80
CMD [ "/usr/local/bin/dashboard" ]

@ -1,2 +1,42 @@
# go-micro-dashboard
Dashboard for go-micro.
# Go Micro Dashboard
Go micro dashboard is designed to make it as easy as possible for users to work with go-micro framework.
# Features
- [ ] Dashboard
- [ ] Services list
- [ ] Support request endpoints
- [ ] Support dashboard authenticate
- [ ] Support configure service
## Development
### Server
#### Swagger
```
swagger generate spec -o docs/swagger.json -b ./docs
swag init
```
### Web UI
[Document](https://github.com/xpunch/go-micro-dashboard/tree/main/frontend)
## Docker
```
docker run .
```
## Docker Compose
```
docker-compose up -d
```
## License
[Apache License 2.0](./LICENSE)

@ -0,0 +1,11 @@
server:
env: "dev"
address: ":4000"
cors:
enable: true
origin: "http://localhost:4200"
swagger:
host: "localhost:4000"
base: "/"
web:
path: "./frontend/dist"

@ -0,0 +1,58 @@
package config
import "time"
const (
EnvDev = "dev"
EnvProd = "prod"
)
type Config struct {
Server ServerConfig
}
type ServerConfig struct {
Env string
Address string
Auth AuthConfig
CORS CORSConfig
Swagger SwaggerConfig
Web WebConfig
}
type AuthConfig struct {
Username string
Password string
TokenSecret string
TokenExpiration time.Duration
}
type CORSConfig struct {
Enable bool `toml:"enable"`
Origin string `toml:"origin"`
}
type SwaggerConfig struct {
Host string
Base string
}
type WebConfig struct {
Path string
}
func GetConfig() Config {
return *_cfg
}
func GetServerConfig() ServerConfig {
return _cfg.Server
}
func GetAuthConfig() AuthConfig {
return _cfg.Server.Auth
}
func GetSwaggerConfig() SwaggerConfig {
return _cfg.Server.Swagger
}

@ -0,0 +1 @@
package config

@ -0,0 +1,96 @@
package config
import (
"os"
"strings"
"time"
"github.com/asim/go-micro/plugins/config/encoder/toml/v4"
"github.com/asim/go-micro/plugins/config/encoder/yaml/v4"
"github.com/pkg/errors"
"github.com/xpunch/go-micro-dashboard/util"
"go-micro.dev/v4/config"
"go-micro.dev/v4/config/reader"
"go-micro.dev/v4/config/reader/json"
"go-micro.dev/v4/config/source/env"
"go-micro.dev/v4/config/source/file"
"go-micro.dev/v4/logger"
)
// internal instance of Config
var _cfg *Config = &Config{
Server: ServerConfig{
Env: EnvProd,
Address: ":4000",
Auth: AuthConfig{
Username: "admin",
Password: "123456",
TokenSecret: "modifyme",
TokenExpiration: 24 * time.Hour,
},
Swagger: SwaggerConfig{
Host: "localhost:4000",
Base: "/",
},
Web: WebConfig{
Path: "web",
},
},
}
// Load will load configurations and update it when changed
func Load() error {
var configor config.Config
var err error
switch strings.ToLower(os.Getenv("CONFIG_TYPE")) {
case "env":
configor, err = config.NewConfig(
config.WithSource(env.NewSource()),
)
case "toml":
filename := "config.toml"
if name := os.Getenv("CONFIG_FILE"); len(name) > 0 {
filename = name
}
configor, err = config.NewConfig(
config.WithSource(file.NewSource(file.WithPath(filename))),
config.WithReader(json.NewReader(reader.WithEncoder(toml.NewEncoder()))),
)
default:
filename := "config.yaml"
if name := os.Getenv("CONFIG_FILE"); len(name) > 0 {
filename = name
}
configor, err = config.NewConfig(
config.WithSource(file.NewSource(file.WithPath(filename))),
config.WithReader(json.NewReader(reader.WithEncoder(yaml.NewEncoder()))),
)
}
if err != nil {
return errors.Wrap(err, "configor.New")
}
if err := configor.Load(); err != nil {
return errors.Wrap(err, "configor.Load")
}
if err := configor.Scan(_cfg); err != nil {
return errors.Wrap(err, "configor.Scan")
}
w, err := configor.Watch()
if err != nil {
return errors.Wrap(err, "configor.Watch")
}
util.GoSafe(func() {
for {
v, err := w.Next()
if err != nil {
logger.Error(err)
return
}
if err := v.Scan(_cfg); err != nil {
logger.Error(err)
return
}
}
})
return nil
}

@ -1,90 +1,17 @@
<p align="center">
<a href="https://ng-alain.com">
<img width="100" src="https://ng-alain.com/assets/img/logo-color.svg">
</a>
</p>
# Go Micro Dashboard
<h1 align="center">NG-ALAIN</h1>
## Development
<div align="center">
Out-of-box UI solution for enterprise applications, Let developers focus on business.
### NG-Alain
[![Build Status](https://dev.azure.com/ng-alain/ng-alain/_apis/build/status/ng-alain-CI?branchName=master)](https://dev.azure.com/ng-alain/ng-alain/_build/latest?definitionId=2&branchName=master)
[![Dependency Status](https://david-dm.org/ng-alain/ng-alain/status.svg?style=flat-square)](https://david-dm.org/ng-alain/ng-alain)
[![GitHub Release Date](https://img.shields.io/github/release-date/ng-alain/ng-alain.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/releases)
[![NPM version](https://img.shields.io/npm/v/ng-alain.svg?style=flat-square)](https://www.npmjs.com/package/ng-alain)
[![prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://prettier.io/)
[![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/blob/master/LICENSE)
[![Gitter](https://img.shields.io/gitter/room/ng-alain/ng-alain.svg?style=flat-square)](https://gitter.im/ng-alain/ng-alain)
[![ng-zorro-vscode](https://img.shields.io/badge/ng--zorro-VSCODE-brightgreen.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-zorro-vscode)
[![ng-alain-vscode](https://img.shields.io/badge/ng--alain-VSCODE-brightgreen.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=cipchk.ng-alain-vscode)
```
ng new go-micro-dashboard
ng add ng-alain
```
</div>
### Swagger
English | [简体中文](README-zh_CN.md)
## Quickstart
- [Getting Started](https://ng-alain.com/docs/getting-started)
## Links
+ [Document](https://ng-alain.com) ([Surge Mirror](https://ng-alain-doc.surge.sh))
+ [@delon Source](https://github.com/ng-alain/delon)
+ [DEMO](https://ng-alain.surge.sh) ([国内镜像](https://ng-alain.gitee.io/))
## Features
+ `ng-zorro-antd` based
+ Responsive Layout
+ I18n
+ [@delon](https://github.com/ng-alain/delon)
+ Lazy load Assets
+ UI Router States
+ Customize Theme
+ Less preprocessor
+ RTL
+ Well organized & commented code
+ Simple upgrade
+ Support Docker deploy
## Architecture
![Architecture](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/architecture.png)
> [delon](https://github.com/ng-alain/delon) is a production-ready solution for admin business components packages, Built on the design principles developed by Ant Design.
## App Shots
![desktop](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/desktop.png)
![ipad](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/ipad.png)
![iphone](https://raw.githubusercontent.com/ng-alain/delon/master/_screenshot/iphone.png)
## Contributing
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/ng-alain/ng-alain/pulls)
We welcome all contributions. Please read our [CONTRIBUTING.md](https://github.com/ng-alain/ng-alain/blob/master/CONTRIBUTING.md) first. You can submit any ideas as [pull requests](https://github.com/ng-alain/ng-alain/pulls) or as [GitHub issues](https://github.com/ng-alain/ng-alain/issues).
> If you're new to posting issues, we ask that you read [*How To Ask Questions The Smart Way*](http://www.catb.org/~esr/faqs/smart-questions.html) (**This guide does not provide actual support services for this project!**), [How to Ask a Question in Open Source Community](https://github.com/seajs/seajs/issues/545) and [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) prior to posting. Well written bug reports help us help you!
## Donation
ng-alain is an MIT-licensed open source project. In order to achieve better and sustainable development of the project, we expect to gain more backers. You can support us in any of the following ways:
- [patreon](https://www.patreon.com/cipchk)
- [opencollective](https://opencollective.com/ng-alain)
- [paypal](https://www.paypal.me/cipchk)
- [支付宝或微信](https://ng-alain.com/assets/donate.png)
Or purchasing our [business theme](https://e.ng-alain.com/).
## Backers
Thank you to all our backers! 🙏
<a href="https://opencollective.com/ng-alain#backers" target="_blank"><img src="https://opencollective.com/ng-alain/backers.svg?width=890"></a>
### License
The MIT License (see the [LICENSE](https://github.com/ng-alain/ng-alain/blob/master/LICENSE) file for the full text)
```
npm install
node_modules/.bin/nswag run
```

@ -52,7 +52,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/go-micro-dashboard",
"outputPath": "../web",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",

@ -0,0 +1,76 @@
{
"runtime": "Default",
"defaultVariables": null,
"documentGenerator": {
"fromDocument": {
"url": "http://localhost:4000/swagger/doc.json",
"output": null,
"newLineBehavior": "Auto"
}
},
"codeGenerators": {
"openApiToTypeScriptClient": {
"className": "{controller}ServiceProxy",
"moduleName": "",
"namespace": "",
"typeScriptVersion": 2.7,
"template": "Angular",
"promiseType": "Promise",
"httpClass": "HttpClient",
"withCredentials": false,
"useSingletonProvider": false,
"injectionTokenType": "InjectionToken",
"rxJsVersion": 6.0,
"dateTimeType": "Date",
"nullValue": "Undefined",
"generateClientClasses": true,
"generateClientInterfaces": false,
"generateOptionalParameters": false,
"exportTypes": true,
"wrapDtoExceptions": false,
"exceptionClass": "ApiException",
"clientBaseClass": null,
"wrapResponses": false,
"wrapResponseMethods": [],
"generateResponseClasses": true,
"responseClass": "SwaggerResponse",
"protectedMethods": [],
"configurationClass": null,
"useTransformOptionsMethod": false,
"useTransformResultMethod": false,
"generateDtoTypes": true,
"operationGenerationMode": "MultipleClientsFromOperationId",
"markOptionalProperties": true,
"generateCloneMethod": false,
"typeStyle": "Class",
"enumStyle": "Enum",
"useLeafType": false,
"classTypes": [],
"extendedClasses": [],
"extensionCode": null,
"generateDefaultValues": true,
"excludedTypeNames": [],
"excludedParameterNames": [],
"handleReferences": false,
"generateConstructorInterface": true,
"convertConstructorInterfaceData": false,
"importRequiredTypes": true,
"useGetBaseUrlMethod": false,
"baseUrlTokenName": "API_BASE_URL",
"queryNullValue": "",
"useAbortSignal": false,
"inlineNamedDictionaries": false,
"inlineNamedAny": false,
"includeHttpContext": false,
"templateDirectory": null,
"typeNameGeneratorType": null,
"propertyNameGeneratorType": null,
"enumNameGeneratorType": null,
"checksumCacheEnabled": false,
"serviceHost": null,
"serviceSchemes": null,
"output": "src/app/shared/service-proxies/service-proxies.ts",
"newLineBehavior": "Auto"
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{
"name": "go-micro-dashboard",
"version": "0.0.0",
"version": "1.0.0",
"scripts": {
"ng": "ng",
"start": "ng s -o",
@ -15,7 +15,6 @@
"color-less": "ng-alain-plugin-theme -t=colorLess",
"theme": "ng-alain-plugin-theme -t=themeCss",
"icon": "ng g ng-alain:plugin icon",
"prepare": "husky install",
"lint": "npm run lint:ts && npm run lint:style",
"lint:ts": "ng lint --fix",
"lint:style": "stylelint \"src/**/*.less\" --syntax less --fix"
@ -30,6 +29,7 @@
"@angular/platform-browser": "~12.2.0",
"@angular/platform-browser-dynamic": "~12.2.0",
"@angular/router": "~12.2.0",
"ng-alain": "^12.3.0",
"rxjs": "~6.6.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4",
@ -48,22 +48,17 @@
},
"devDependencies": {
"@angular-devkit/build-angular": "~12.2.12",
"@angular/cli": "~12.2.12",
"@angular/compiler-cli": "~12.2.0",
"@types/jasmine": "~3.8.0",
"@types/node": "^12.11.1",
"jasmine-core": "~3.8.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.3.5",
"@angular-eslint/builder": "~12.3.1",
"@angular-eslint/eslint-plugin": "~12.3.1",
"@angular-eslint/eslint-plugin-template": "~12.3.1",
"@angular-eslint/schematics": "~12.3.1",
"@angular-eslint/template-parser": "~12.3.1",
"@angular/cli": "~12.2.12",
"@angular/compiler-cli": "~12.2.0",
"@angular/language-service": "~12.2.0",
"@delon/testing": "^12.3.0",
"@types/jasmine": "~3.8.0",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "~4.29.2",
"@typescript-eslint/parser": "~4.29.2",
"eslint": "^7.32.0",
@ -72,20 +67,25 @@
"eslint-plugin-jsdoc": "~36.0.7",
"eslint-plugin-prefer-arrow": "~1.2.3",
"eslint-plugin-prettier": "^2.2.1",
"prettier": "^2.2.1",
"husky": "^6.0.0",
"jasmine-core": "~3.8.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"lint-staged": "^11.1.2",
"ng-alain": "^12.3.0",
"ng-alain-plugin-theme": "^12.0.0",
"nswag": "^13.14.0",
"prettier": "^2.2.1",
"source-map-explorer": "^2.5.2",
"@angular/language-service": "~12.2.0",
"@delon/testing": "^12.3.0",
"lint-staged": "^11.1.2",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.4.0",
"stylelint-order": "^4.1.0"
"stylelint-order": "^4.1.0",
"typescript": "~4.3.5"
},
"lint-staged": {
"(src)/**/*.{html,ts}": [
@ -95,4 +95,4 @@
"stylelint --syntax less --fix"
]
}
}
}

@ -34,7 +34,7 @@ const LANG_PROVIDES = [
// #region JSON Schema form (using @delon/form)
import { JsonSchemaModule } from '@shared';
const FORM_MODULES = [ JsonSchemaModule ];
const FORM_MODULES = [JsonSchemaModule];
// #endregion
@ -43,8 +43,8 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { DefaultInterceptor } from '@core';
import { SimpleInterceptor } from '@delon/auth';
const INTERCEPTOR_PROVIDES = [
{ provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true},
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true}
{ provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true }
];
// #endregion
@ -68,6 +68,12 @@ const APPINIT_PROVIDES = [
];
// #endregion
import { environment } from '@env/environment';
import { API_BASE_URL } from './shared/service-proxies/service-proxies';
export function getRemoteServiceBaseUrl(): string {
return environment.api.baseUrl;
}
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { GlobalConfigModule } from './global-config.module';
@ -98,7 +104,8 @@ import { STWidgetModule } from './shared/st-widget/st-widget.module';
providers: [
...LANG_PROVIDES,
...INTERCEPTOR_PROVIDES,
...APPINIT_PROVIDES
...APPINIT_PROVIDES,
{ provide: API_BASE_URL, useFactory: getRemoteServiceBaseUrl },
],
bootstrap: [AppComponent]
})

@ -172,33 +172,8 @@ export class DefaultInterceptor implements HttpInterceptor {
}
private handleData(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
this.checkStatus(ev);
// 业务处理:一些通用操作
switch (ev.status) {
case 200:
// 业务层级错误处理以下是假定restful有一套统一输出格式指不管成功与否都有相应的数据格式情况下进行处理
// 例如响应内容:
// 错误内容:{ status: 1, msg: '非法参数' }
// 正确内容:{ status: 0, response: { } }
// 则以下代码片断可直接适用
// if (ev instanceof HttpResponse) {
// const body = ev.body;
// if (body && body.status !== 0) {
// this.injector.get(NzMessageService).error(body.msg);
// // 注意这里如果继续抛出错误会被行254的 catchError 二次拦截,导致外部实现的 Pipe、subscribe 操作被中断例如this.http.get('/').subscribe() 不会触发
// // 如果你希望外部实现需要手动移除行254
// return throwError({});
// } else {
// // 忽略 Blob 文件体
// if (ev.body instanceof Blob) {
// return of(ev);
// }
// // 重新修改 `body` 内容为 `response` 内容,对于绝大多数场景已经无须再关心业务状态码
// return of(new HttpResponse(Object.assign(ev, { body: body.response })));
// // 或者依然保持完整的格式
// return of(ev);
// }
// }
break;
case 401:
if (this.refreshTokenEnabled && this.refreshTokenType === 're-request') {
@ -208,15 +183,16 @@ export class DefaultInterceptor implements HttpInterceptor {
break;
case 403:
case 404:
case 500:
this.goTo(`/exception/${ev.status}`);
break;
default:
console.log(ev);
if (ev instanceof HttpErrorResponse) {
console.warn(
'未可知错误大部分是由于后端不支持跨域CORS或无效配置引起请参考 https://ng-alain.com/docs/server 解决跨域问题',
ev
);
this.blobToText(ev.error).subscribe((resp) => {
var msg = resp ? resp : ev.statusText;
this.notification.error('Error', `${ev.status}: ${ev.url} ${msg}`, { nzDuration: 5000 });
});
return throwError(ev);
}
break;
}
@ -257,4 +233,20 @@ export class DefaultInterceptor implements HttpInterceptor {
catchError((err: HttpErrorResponse) => this.handleData(err, newReq, next))
);
}
blobToText(blob: any): Observable<string> {
return new Observable<string>((observer: any) => {
if (blob instanceof Blob) {
let reader = new FileReader();
reader.onload = (event) => {
observer.next((<any>event.target).result);
observer.complete();
};
reader.readAsText(blob);
} else {
observer.next(blob && blob.error ? blob.error : JSON.stringify(blob));
observer.complete();
}
});
}
}

@ -3,15 +3,9 @@
<div class="top">
<div class="head">
<img class="logo" src="./assets/logo-color.svg">
<span class="title">ng-alain</span>
<span class="title">Go Micro Dashboard</span>
</div>
<div class="desc">武林中最有影响力的《葵花宝典》;欲练神功,挥刀自宫</div>
</div>
<router-outlet></router-outlet>
<global-footer [links]="links">
Copyright
<i class="anticon anticon-copyright"></i> 2021
<a href="//github.com/cipchk" target="_blank">卡色</a>出品
</global-footer>
</div>
</div>

@ -7,21 +7,6 @@ import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
styleUrls: ['./passport.component.less']
})
export class LayoutPassportComponent implements OnInit {
links = [
{
title: '帮助',
href: ''
},
{
title: '隐私',
href: ''
},
{
title: '条款',
href: ''
}
];
constructor(@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService) {}
ngOnInit(): void {

@ -3,61 +3,25 @@
<nz-tab [nzTitle]="'Credentials'">
<nz-alert *ngIf="error" [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true" class="mb-lg"></nz-alert>
<nz-form-item>
<nz-form-control nzErrorTip="Please enter mobile number, muse be: admin or user">
<nz-form-control nzErrorTip="Please enter username">
<nz-input-group nzSize="large" nzPrefixIcon="user">
<input nz-input formControlName="userName" placeholder="username: admin or user" />
<input nz-input formControlName="username" placeholder="username" />
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control nzErrorTip="Please enter password, muse be: ng-alain.com">
<nz-form-control nzErrorTip="Please enter password">
<nz-input-group nzSize="large" nzPrefixIcon="lock">
<input nz-input type="password" formControlName="password" placeholder="password: ng-alain.com" />
<input nz-input type="password" formControlName="password" placeholder="password" />
</nz-input-group>
</nz-form-control>
</nz-form-item>
</nz-tab>
<nz-tab [nzTitle]="'Mobile number'">
<nz-form-item>
<nz-form-control [nzErrorTip]="mobileErrorTip">
<nz-input-group nzSize="large" nzPrefixIcon="user">
<input nz-input formControlName="mobile" placeholder="mobile number" />
</nz-input-group>
<ng-template #mobileErrorTip let-i>
<ng-container *ngIf="i.errors.required">
Please enter your phone number!
</ng-container>
<ng-container *ngIf="i.errors.pattern">
Malformed phone number!
</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="'Please enter the verification code!'">
<nz-row [nzGutter]="8">
<nz-col [nzSpan]="16">
<nz-input-group nzSize="large" nzPrefixIcon="mail">
<input nz-input formControlName="captcha" placeholder="captcha" />
</nz-input-group>
</nz-col>
<nz-col [nzSpan]="8">
<button type="button" nz-button nzSize="large" (click)="getCaptcha()" [disabled]="count >= 0" nzBlock [nzLoading]="loading">
{{ count ? count + 's' : ('Get code') }}
</button>
</nz-col>
</nz-row>
</nz-form-control>
</nz-form-item>
</nz-tab>
</nz-tabset>
<nz-form-item>
<nz-col [nzSpan]="12">
<label nz-checkbox formControlName="remember">Remember me</label>
</nz-col>
<nz-col [nzSpan]="12" class="text-right">
<a class="forgot" routerLink="/passport/register">Forgot your password?</a>
</nz-col>
</nz-form-item>
<nz-form-item>
<button nz-button type="submit" nzType="primary" nzSize="large" [nzLoading]="loading" nzBlock>
@ -65,10 +29,3 @@
</button>
</nz-form-item>
</form>
<div class="other">
Sign in with
<i nz-tooltip nzTooltipTitle="in fact Auth0 via window" (click)="open('auth0', 'window')" nz-icon nzType="alipay-circle" class="icon"></i>
<i nz-tooltip nzTooltipTitle="in fact Github via redirect" (click)="open('github')" nz-icon nzType="taobao-circle" class="icon"></i>
<i (click)="open('weibo', 'window')" nz-icon nzType="weibo-circle" class="icon"></i>
<a class="register" routerLink="/passport/register">Sign up</a>
</div>

@ -8,6 +8,7 @@ import { SettingsService, _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { NzTabChangeEvent } from 'ng-zorro-antd/tabs';
import { finalize } from 'rxjs/operators';
import { AccountServiceProxy, LoginRequest } from 'src/app/shared/service-proxies/service-proxies';
@Component({
selector: 'passport-login',
@ -16,7 +17,7 @@ import { finalize } from 'rxjs/operators';
providers: [SocialService],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserLoginComponent implements OnDestroy {
export class UserLoginComponent {
constructor(
fb: FormBuilder,
private router: Router,
@ -28,31 +29,24 @@ export class UserLoginComponent implements OnDestroy {
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
private startupSrv: StartupService,
private http: _HttpClient,
private cdr: ChangeDetectorRef
private cdr: ChangeDetectorRef,
private accountService: AccountServiceProxy,
) {
this.form = fb.group({
userName: [null, [Validators.required, Validators.pattern(/^(admin|user)$/)]],
password: [null, [Validators.required, Validators.pattern(/^(ng\-alain\.com)$/)]],
mobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]],
captcha: [null, [Validators.required]],
username: [null, [Validators.required]],
password: [null, [Validators.required]],
remember: [true]
});
}
// #region fields
get userName(): AbstractControl {
return this.form.controls.userName;
get username(): AbstractControl {
return this.form.controls.username;
}
get password(): AbstractControl {
return this.form.controls.password;
}
get mobile(): AbstractControl {
return this.form.controls.mobile;
}
get captcha(): AbstractControl {
return this.form.controls.captcha;
}
form: FormGroup;
error = '';
type = 0;
@ -61,7 +55,6 @@ export class UserLoginComponent implements OnDestroy {
// #region get captcha
count = 0;
interval$: any;
// #endregion
@ -69,72 +62,36 @@ export class UserLoginComponent implements OnDestroy {
this.type = index!;
}
getCaptcha(): void {
if (this.mobile.invalid) {
this.mobile.markAsDirty({ onlySelf: true });
this.mobile.updateValueAndValidity({ onlySelf: true });
return;
}
this.count = 59;
this.interval$ = setInterval(() => {
this.count -= 1;
if (this.count <= 0) {
clearInterval(this.interval$);
}
}, 1000);
}
// #endregion
submit(): void {
this.error = '';
if (this.type === 0) {
this.userName.markAsDirty();
this.userName.updateValueAndValidity();
this.username.markAsDirty();
this.username.updateValueAndValidity();
this.password.markAsDirty();
this.password.updateValueAndValidity();
if (this.userName.invalid || this.password.invalid) {
return;
}
} else {
this.mobile.markAsDirty();
this.mobile.updateValueAndValidity();
this.captcha.markAsDirty();
this.captcha.updateValueAndValidity();
if (this.mobile.invalid || this.captcha.invalid) {
if (this.username.invalid || this.password.invalid) {
return;
}
}
// 默认配置中对所有HTTP请求都会强制 [校验](https://ng-alain.com/auth/getting-started) 用户 Token
// 然一般来说登录请求不需要校验因此可以在请求URL加上`/login?_allow_anonymous=true` 表示不触发用户 Token 校验
this.loading = true;
this.cdr.detectChanges();
this.http
.post('/login/account?_allow_anonymous=true', {
type: this.type,
userName: this.userName.value,
password: this.password.value
})
.pipe(
finalize(() => {
this.loading = true;
this.cdr.detectChanges();
})
)
.subscribe(res => {
if (res.msg !== 'ok') {
this.error = res.msg;
this.accountService.login(new LoginRequest({ username: this.username.value, password: this.password.value }))
.pipe(finalize(() => {
this.loading = false;
this.cdr.detectChanges();
}))
.subscribe(resp => {
console.log("failed")
if (!resp.token) {
this.error = "login failed";
this.cdr.detectChanges();
return;
}
// 清空路由复用信息
this.reuseTabService.clear();
// 设置用户Token信息
// TODO: Mock expired value
res.user.expired = +new Date() + 1000 * 60 * 5;
this.tokenService.set(res.user);
// 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响
this.tokenService.set({ token: resp.token });
this.startupSrv.load().subscribe(() => {
let url = this.tokenService.referrer!.url || '/';
if (url.includes('/passport')) {
@ -144,53 +101,4 @@ export class UserLoginComponent implements OnDestroy {
});
});
}
// #region social
open(type: string, openType: SocialOpenType = 'href'): void {
let url = ``;
let callback = ``;
if (environment.production) {
callback = `https://ng-alain.github.io/ng-alain/#/passport/callback/${type}`;
} else {
callback = `http://localhost:4200/#/passport/callback/${type}`;
}
switch (type) {
case 'auth0':
url = `//cipchk.auth0.com/login?client=8gcNydIDzGBYxzqV0Vm1CX_RXH-wsWo5&redirect_uri=${decodeURIComponent(callback)}`;
break;
case 'github':
url = `//github.com/login/oauth/authorize?client_id=9d6baae4b04a23fcafa2&response_type=code&redirect_uri=${decodeURIComponent(
callback
)}`;
break;
case 'weibo':
url = `https://api.weibo.com/oauth2/authorize?client_id=1239507802&response_type=code&redirect_uri=${decodeURIComponent(callback)}`;
break;
}
if (openType === 'window') {
this.socialService
.login(url, '/', {
type: 'window'
})
.subscribe(res => {
if (res) {
this.settingsService.setUser(res);
this.router.navigateByUrl('/');
}
});
} else {
this.socialService.login(url, '/', {
type: 'href'
});
}
}
// #endregion
ngOnDestroy(): void {
if (this.interval$) {
clearInterval(this.interval$);
}
}
}

@ -0,0 +1,815 @@
/* tslint:disable */
/* eslint-disable */
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v13.14.0.0 (NJsonSchema v10.5.2.0 (Newtonsoft.Json v11.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
// ReSharper disable InconsistentNaming
import { mergeMap as _observableMergeMap, catchError as _observableCatch } from 'rxjs/operators';
import { Observable, throwError as _observableThrow, of as _observableOf } from 'rxjs';
import { Injectable, Inject, Optional, InjectionToken } from '@angular/core';
import { HttpClient, HttpHeaders, HttpResponse, HttpResponseBase } from '@angular/common/http';
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');
@Injectable()
export class AccountServiceProxy {
private http: HttpClient;
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
this.http = http;
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "localhost:4000/";
}
/**
* @param input request
* @return success
*/
login(input: LoginRequest) : Observable<LoginResponse> {
let url_ = this.baseUrl + "/api/login";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(input);
let options_ : any = {
body: content_,
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Content-Type": "application/json",
"Accept": "application/json"
})
};
return this.http.request("post", url_, options_).pipe(_observableMergeMap((response_ : any) => {
return this.processLogin(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processLogin(<any>response_);
} catch (e) {
return <Observable<LoginResponse>><any>_observableThrow(e);
}
} else
return <Observable<LoginResponse>><any>_observableThrow(response_);
}));
}
protected processLogin(response: HttpResponseBase): Observable<LoginResponse> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }}
if (status === 200) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result200: any = null;
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result200 = LoginResponse.fromJS(resultData200);
return _observableOf(result200);
}));
} else if (status === 400) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result400: any = null;
let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result400 = resultData400 !== undefined ? resultData400 : <any>null;
return throwException("Bad Request", status, _responseText, _headers, result400);
}));
} else if (status === 401) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result401: any = null;
let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result401 = resultData401 !== undefined ? resultData401 : <any>null;
return throwException("Unauthorized", status, _responseText, _headers, result401);
}));
} else if (status === 500) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result500: any = null;
let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result500 = resultData500 !== undefined ? resultData500 : <any>null;
return throwException("Internal Server Error", status, _responseText, _headers, result500);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}));
}
return _observableOf<LoginResponse>(<any>null);
}
}
@Injectable()
export class RegistryServiceProxy {
private http: HttpClient;
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
this.http = http;
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "localhost:4000/";
}
/**
* @return OK
*/
getServices() : Observable<GetServiceListResponse> {
let url_ = this.baseUrl + "/api/registry/services";
url_ = url_.replace(/[?&]$/, "");
let options_ : any = {
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Accept": "application/json"
})
};
return this.http.request("get", url_, options_).pipe(_observableMergeMap((response_ : any) => {
return this.processGetServices(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processGetServices(<any>response_);
} catch (e) {
return <Observable<GetServiceListResponse>><any>_observableThrow(e);
}
} else
return <Observable<GetServiceListResponse>><any>_observableThrow(response_);
}));
}
protected processGetServices(response: HttpResponseBase): Observable<GetServiceListResponse> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }}
if (status === 200) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result200: any = null;
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result200 = GetServiceListResponse.fromJS(resultData200);
return _observableOf(result200);
}));
} else if (status === 400) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result400: any = null;
let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result400 = resultData400 !== undefined ? resultData400 : <any>null;
return throwException("Bad Request", status, _responseText, _headers, result400);
}));
} else if (status === 401) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result401: any = null;
let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result401 = resultData401 !== undefined ? resultData401 : <any>null;
return throwException("Unauthorized", status, _responseText, _headers, result401);
}));
} else if (status === 500) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result500: any = null;
let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result500 = resultData500 !== undefined ? resultData500 : <any>null;
return throwException("Internal Server Error", status, _responseText, _headers, result500);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}));
}
return _observableOf<GetServiceListResponse>(<any>null);
}
}
@Injectable()
export class StatisticsServiceProxy {
private http: HttpClient;
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
this.http = http;
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "localhost:4000/";
}
/**
* @return OK
*/
getSummary() : Observable<GetSummaryResponse> {
let url_ = this.baseUrl + "/api/summary";
url_ = url_.replace(/[?&]$/, "");
let options_ : any = {
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Accept": "application/json"
})
};
return this.http.request("get", url_, options_).pipe(_observableMergeMap((response_ : any) => {
return this.processGetSummary(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processGetSummary(<any>response_);
} catch (e) {
return <Observable<GetSummaryResponse>><any>_observableThrow(e);
}
} else
return <Observable<GetSummaryResponse>><any>_observableThrow(response_);
}));
}
protected processGetSummary(response: HttpResponseBase): Observable<GetSummaryResponse> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }}
if (status === 200) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result200: any = null;
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result200 = GetSummaryResponse.fromJS(resultData200);
return _observableOf(result200);
}));
} else if (status === 400) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result400: any = null;
let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result400 = resultData400 !== undefined ? resultData400 : <any>null;
return throwException("Bad Request", status, _responseText, _headers, result400);
}));
} else if (status === 401) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result401: any = null;
let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result401 = resultData401 !== undefined ? resultData401 : <any>null;
return throwException("Unauthorized", status, _responseText, _headers, result401);
}));
} else if (status === 500) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result500: any = null;
let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result500 = resultData500 !== undefined ? resultData500 : <any>null;
return throwException("Internal Server Error", status, _responseText, _headers, result500);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}));
}
return _observableOf<GetSummaryResponse>(<any>null);
}
}
export class LoginRequest implements ILoginRequest {
password!: string;
username!: string;
constructor(data?: ILoginRequest) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.password = _data["password"];
this.username = _data["username"];
}
}
static fromJS(data: any): LoginRequest {
data = typeof data === 'object' ? data : {};
let result = new LoginRequest();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["password"] = this.password;
data["username"] = this.username;
return data;
}
}
export interface ILoginRequest {
password: string;
username: string;
}
export class LoginResponse implements ILoginResponse {
token?: string | undefined;
constructor(data?: ILoginResponse) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.token = _data["token"];
}
}
static fromJS(data: any): LoginResponse {
data = typeof data === 'object' ? data : {};
let result = new LoginResponse();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["token"] = this.token;
return data;
}
}
export interface ILoginResponse {
token?: string | undefined;
}
export class GetServiceListResponse implements IGetServiceListResponse {
services?: RegistryService[] | undefined;
constructor(data?: IGetServiceListResponse) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
if (Array.isArray(_data["services"])) {
this.services = [] as any;
for (let item of _data["services"])
this.services!.push(RegistryService.fromJS(item));
}
}
}
static fromJS(data: any): GetServiceListResponse {
data = typeof data === 'object' ? data : {};
let result = new GetServiceListResponse();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
if (Array.isArray(this.services)) {
data["services"] = [];
for (let item of this.services)
data["services"].push(item.toJSON());
}
return data;
}
}
export interface IGetServiceListResponse {
services?: RegistryService[] | undefined;
}
export class RegistryEndpoint implements IRegistryEndpoint {
metadata?: { [key: string]: string; } | undefined;
name?: string | undefined;
request?: RegistryValue | undefined;
response?: RegistryValue | undefined;
constructor(data?: IRegistryEndpoint) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
if (_data["metadata"]) {
this.metadata = {} as any;
for (let key in _data["metadata"]) {
if (_data["metadata"].hasOwnProperty(key))
(<any>this.metadata)![key] = _data["metadata"][key];
}
}
this.name = _data["name"];
this.request = _data["request"] ? RegistryValue.fromJS(_data["request"]) : <any>undefined;
this.response = _data["response"] ? RegistryValue.fromJS(_data["response"]) : <any>undefined;
}
}
static fromJS(data: any): RegistryEndpoint {
data = typeof data === 'object' ? data : {};
let result = new RegistryEndpoint();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
if (this.metadata) {
data["metadata"] = {};
for (let key in this.metadata) {
if (this.metadata.hasOwnProperty(key))
(<any>data["metadata"])[key] = this.metadata[key];
}
}
data["name"] = this.name;
data["request"] = this.request ? this.request.toJSON() : <any>undefined;
data["response"] = this.response ? this.response.toJSON() : <any>undefined;
return data;
}
}
export interface IRegistryEndpoint {
metadata?: { [key: string]: string; } | undefined;
name?: string | undefined;
request?: RegistryValue | undefined;
response?: RegistryValue | undefined;
}
export class RegistryNode implements IRegistryNode {
address?: string | undefined;
id?: string | undefined;
metadata?: { [key: string]: string; } | undefined;
constructor(data?: IRegistryNode) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.address = _data["address"];
this.id = _data["id"];
if (_data["metadata"]) {
this.metadata = {} as any;
for (let key in _data["metadata"]) {
if (_data["metadata"].hasOwnProperty(key))
(<any>this.metadata)![key] = _data["metadata"][key];
}
}
}
}
static fromJS(data: any): RegistryNode {
data = typeof data === 'object' ? data : {};
let result = new RegistryNode();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["address"] = this.address;
data["id"] = this.id;
if (this.metadata) {
data["metadata"] = {};
for (let key in this.metadata) {
if (this.metadata.hasOwnProperty(key))
(<any>data["metadata"])[key] = this.metadata[key];
}
}
return data;
}
}
export interface IRegistryNode {
address?: string | undefined;
id?: string | undefined;
metadata?: { [key: string]: string; } | undefined;
}
export class RegistryService implements IRegistryService {
endpoints?: RegistryEndpoint[] | undefined;
metadata?: { [key: string]: string; } | undefined;
name?: string | undefined;
nodes?: RegistryNode[] | undefined;
version?: string | undefined;
constructor(data?: IRegistryService) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
if (Array.isArray(_data["endpoints"])) {
this.endpoints = [] as any;
for (let item of _data["endpoints"])
this.endpoints!.push(RegistryEndpoint.fromJS(item));
}
if (_data["metadata"]) {
this.metadata = {} as any;
for (let key in _data["metadata"]) {
if (_data["metadata"].hasOwnProperty(key))
(<any>this.metadata)![key] = _data["metadata"][key];
}
}
this.name = _data["name"];
if (Array.isArray(_data["nodes"])) {
this.nodes = [] as any;
for (let item of _data["nodes"])
this.nodes!.push(RegistryNode.fromJS(item));
}
this.version = _data["version"];
}
}
static fromJS(data: any): RegistryService {
data = typeof data === 'object' ? data : {};
let result = new RegistryService();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
if (Array.isArray(this.endpoints)) {
data["endpoints"] = [];
for (let item of this.endpoints)
data["endpoints"].push(item.toJSON());
}
if (this.metadata) {
data["metadata"] = {};
for (let key in this.metadata) {
if (this.metadata.hasOwnProperty(key))
(<any>data["metadata"])[key] = this.metadata[key];
}
}
data["name"] = this.name;
if (Array.isArray(this.nodes)) {
data["nodes"] = [];
for (let item of this.nodes)
data["nodes"].push(item.toJSON());
}
data["version"] = this.version;
return data;
}
}
export interface IRegistryService {
endpoints?: RegistryEndpoint[] | undefined;
metadata?: { [key: string]: string; } | undefined;
name?: string | undefined;
nodes?: RegistryNode[] | undefined;
version?: string | undefined;
}
export class RegistryValue implements IRegistryValue {
name?: string | undefined;
type?: string | undefined;
values?: RegistryValue[] | undefined;
constructor(data?: IRegistryValue) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.name = _data["name"];
this.type = _data["type"];
if (Array.isArray(_data["values"])) {
this.values = [] as any;
for (let item of _data["values"])
this.values!.push(RegistryValue.fromJS(item));
}
}
}
static fromJS(data: any): RegistryValue {
data = typeof data === 'object' ? data : {};
let result = new RegistryValue();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["name"] = this.name;
data["type"] = this.type;
if (Array.isArray(this.values)) {
data["values"] = [];
for (let item of this.values)
data["values"].push(item.toJSON());
}
return data;
}
}
export interface IRegistryValue {
name?: string | undefined;
type?: string | undefined;
values?: RegistryValue[] | undefined;
}
export class GetSummaryResponse implements IGetSummaryResponse {
registry?: RegistrySummary | undefined;
services?: ServicesSummary | undefined;
constructor(data?: IGetSummaryResponse) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.registry = _data["registry"] ? RegistrySummary.fromJS(_data["registry"]) : <any>undefined;
this.services = _data["services"] ? ServicesSummary.fromJS(_data["services"]) : <any>undefined;
}
}
static fromJS(data: any): GetSummaryResponse {
data = typeof data === 'object' ? data : {};
let result = new GetSummaryResponse();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["registry"] = this.registry ? this.registry.toJSON() : <any>undefined;
data["services"] = this.services ? this.services.toJSON() : <any>undefined;
return data;
}
}
export interface IGetSummaryResponse {
registry?: RegistrySummary | undefined;
services?: ServicesSummary | undefined;
}
export class RegistrySummary implements IRegistrySummary {
addrs?: string[] | undefined;
type?: string | undefined;
constructor(data?: IRegistrySummary) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
if (Array.isArray(_data["addrs"])) {
this.addrs = [] as any;
for (let item of _data["addrs"])
this.addrs!.push(item);
}
this.type = _data["type"];
}
}
static fromJS(data: any): RegistrySummary {
data = typeof data === 'object' ? data : {};
let result = new RegistrySummary();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
if (Array.isArray(this.addrs)) {
data["addrs"] = [];
for (let item of this.addrs)
data["addrs"].push(item);
}
data["type"] = this.type;
return data;
}
}
export interface IRegistrySummary {
addrs?: string[] | undefined;
type?: string | undefined;
}
export class ServicesSummary implements IServicesSummary {
count?: number | undefined;
nodes_count?: number | undefined;
constructor(data?: IServicesSummary) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.count = _data["count"];
this.nodes_count = _data["nodes_count"];
}
}
static fromJS(data: any): ServicesSummary {
data = typeof data === 'object' ? data : {};
let result = new ServicesSummary();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["count"] = this.count;
data["nodes_count"] = this.nodes_count;
return data;
}
}
export interface IServicesSummary {
count?: number | undefined;
nodes_count?: number | undefined;
}
export class ApiException extends Error {
message: string;
status: number;
response: string;
headers: { [key: string]: any; };
result: any;
constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) {
super();
this.message = message;
this.status = status;
this.response = response;
this.headers = headers;
this.result = result;
}
protected isApiException = true;
static isApiException(obj: any): obj is ApiException {
return obj.isApiException === true;
}
}
function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): Observable<any> {
if (result !== null && result !== undefined)
return _observableThrow(result);
else
return _observableThrow(new ApiException(message, status, response, headers, null));
}
function blobToText(blob: any): Observable<string> {
return new Observable<string>((observer: any) => {
if (!blob) {
observer.next("");
observer.complete();
} else {
let reader = new FileReader();
reader.onload = event => {
observer.next((<any>event.target).result);
observer.complete();
};
reader.readAsText(blob);
}
});
}

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import * as ApiServiceProxies from './service-proxies';
@NgModule({
providers: [
ApiServiceProxies.AccountServiceProxy,
ApiServiceProxies.RegistryServiceProxy,
ApiServiceProxies.StatisticsServiceProxy,
]
})
export class ServiceProxyModule {
}

@ -9,6 +9,8 @@ import { DelonFormModule } from '@delon/form';
import { SHARED_DELON_MODULES } from './shared-delon.module';
import { SHARED_ZORRO_MODULES } from './shared-zorro.module';
import { ServiceProxyModule } from './service-proxies/service-proxy.module';
// #region third libs
const THIRDMODULES: Array<Type<void>> = [];
@ -31,6 +33,7 @@ const DIRECTIVES: Array<Type<void>> = [];
AlainThemeModule.forChild(),
DelonACLModule,
DelonFormModule,
ServiceProxyModule,
...SHARED_DELON_MODULES,
...SHARED_ZORRO_MODULES,
// third libs
@ -49,13 +52,14 @@ const DIRECTIVES: Array<Type<void>> = [];
AlainThemeModule,
DelonACLModule,
DelonFormModule,
ServiceProxyModule,
...SHARED_DELON_MODULES,
...SHARED_ZORRO_MODULES,
// third libs
...THIRDMODULES,
// your components
...COMPONENTS,
...DIRECTIVES
...DIRECTIVES,
]
})
export class SharedModule { }

@ -4,7 +4,7 @@ export const environment = {
production: true,
useHash: true,
api: {
baseUrl: './',
baseUrl: '',
refreshTokenEnabled: true,
refreshTokenType: 'auth-refresh'
}

@ -11,7 +11,7 @@ export const environment = {
production: false,
useHash: true,
api: {
baseUrl: './',
baseUrl: 'http://localhost:4000',
refreshTokenEnabled: true,
refreshTokenType: 'auth-refresh'
},

@ -1,14 +1,117 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>GoMicroDashboard</title>
<title>Go Micro Dashboard</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<style type="text/css">.preloader{position:fixed;top:0;left:0;width:100%;height:100%;overflow:hidden;background:#49a9ee;z-index:9999;transition:opacity .65s}.preloader-hidden-add{opacity:1;display:block}.preloader-hidden-add-active{opacity:0}.preloader-hidden{display:none}.cs-loader{position:absolute;top:0;left:0;height:100%;width:100%}.cs-loader-inner{transform:translateY(-50%);top:50%;position:absolute;width:100%;color:#fff;text-align:center}.cs-loader-inner label{font-size:20px;opacity:0;display:inline-block}@keyframes lol{0%{opacity:0;transform:translateX(-300px)}33%{opacity:1;transform:translateX(0)}66%{opacity:1;transform:translateX(0)}100%{opacity:0;transform:translateX(300px)}}.cs-loader-inner label:nth-child(6){animation:lol 3s infinite ease-in-out}.cs-loader-inner label:nth-child(5){animation:lol 3s .1s infinite ease-in-out}.cs-loader-inner label:nth-child(4){animation:lol 3s .2s infinite ease-in-out}.cs-loader-inner label:nth-child(3){animation:lol 3s .3s infinite ease-in-out}.cs-loader-inner label:nth-child(2){animation:lol 3s .4s infinite ease-in-out}.cs-loader-inner label:nth-child(1){animation:lol 3s .5s infinite ease-in-out}</style></head>
<style type="text/css">
.preloader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #49a9ee;
z-index: 9999;
transition: opacity .65s
}
.preloader-hidden-add {
opacity: 1;
display: block
}
.preloader-hidden-add-active {
opacity: 0
}
.preloader-hidden {
display: none
}
.cs-loader {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%
}
.cs-loader-inner {
transform: translateY(-50%);
top: 50%;
position: absolute;
width: 100%;
color: #fff;
text-align: center
}
.cs-loader-inner label {
font-size: 20px;
opacity: 0;
display: inline-block
}
@keyframes lol {
0% {
opacity: 0;
transform: translateX(-300px)
}
33% {
opacity: 1;
transform: translateX(0)
}
66% {
opacity: 1;
transform: translateX(0)
}
100% {
opacity: 0;
transform: translateX(300px)
}
}
.cs-loader-inner label:nth-child(6) {
animation: lol 3s infinite ease-in-out
}
.cs-loader-inner label:nth-child(5) {
animation: lol 3s .1s infinite ease-in-out
}
.cs-loader-inner label:nth-child(4) {
animation: lol 3s .2s infinite ease-in-out
}
.cs-loader-inner label:nth-child(3) {
animation: lol 3s .3s infinite ease-in-out
}
.cs-loader-inner label:nth-child(2) {
animation: lol 3s .4s infinite ease-in-out
}
.cs-loader-inner label:nth-child(1) {
animation: lol 3s .5s infinite ease-in-out
}
</style>
</head>
<body>
<app-root></app-root>
<div class="preloader"><div class="cs-loader"><div class="cs-loader-inner"><label></label><label></label><label></label><label></label><label></label><label></label></div></div></div>
<div class="preloader">
<div class="cs-loader">
<div class="cs-loader-inner"><label></label><label></label><label></label><label></label><label>
</label><label></label></div>
</div>
</div>
</body>
</html>
</html>

@ -0,0 +1,91 @@
module github.com/xpunch/go-micro-dashboard
go 1.17
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/asim/go-micro/plugins/client/grpc/v4 v4.0.0-20211103025805-c5be9f560cdb
github.com/asim/go-micro/plugins/config/encoder/toml/v4 v4.0.0-20211103025805-c5be9f560cdb
github.com/asim/go-micro/plugins/config/encoder/yaml/v4 v4.0.0-20211103025805-c5be9f560cdb
github.com/asim/go-micro/plugins/registry/etcd/v4 v4.0.0-20211103025805-c5be9f560cdb
github.com/asim/go-micro/plugins/registry/kubernetes/v4 v4.0.0-20211101090014-adaa98e6cffe
github.com/asim/go-micro/plugins/server/http/v4 v4.0.0-20211103025805-c5be9f560cdb
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/static v0.0.1
github.com/gin-gonic/gin v1.7.4
github.com/pkg/errors v0.9.1
github.com/swaggo/gin-swagger v1.3.3
github.com/swaggo/swag v1.7.4
go-micro.dev/v4 v4.3.0
)
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/spec v0.20.3 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/hashstructure v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/tools v0.1.2 // indirect
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
google.golang.org/grpc v1.42.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

1036
go.sum

File diff suppressed because it is too large Load Diff

@ -0,0 +1,63 @@
package account
import (
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
"github.com/xpunch/go-micro-dashboard/config"
"github.com/xpunch/go-micro-dashboard/handler/route"
)
type service struct{}
func NewRouteRegistrar() route.Registrar {
return service{}
}
func (s service) RegisterRoute(router gin.IRoutes) {
router.POST("/api/login", s.Login)
}
type loginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type loginResponse struct {
Token string `json:"token"`
}
// @Tags Account
// @ID account_login
// @Param input body loginRequest true "request"
// @Success 200 {object} loginResponse "success"
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/login [post]
func (s *service) Login(ctx *gin.Context) {
var req loginRequest
if err := ctx.ShouldBindJSON(&req); nil != err {
ctx.Render(400, render.String{Format: err.Error()})
return
}
if req.Username != config.GetServerConfig().Auth.Username ||
req.Password != config.GetServerConfig().Auth.Password {
ctx.Render(400, render.String{Format: "incorrect username or password"})
return
}
claims := jwt.StandardClaims{
Subject: req.Username,
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(config.GetAuthConfig().TokenExpiration).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString([]byte(config.GetAuthConfig().TokenSecret))
if err != nil {
ctx.Render(400, render.String{Format: err.Error()})
return
}
ctx.JSON(200, loginResponse{Token: signedToken})
}

@ -0,0 +1,51 @@
package handler
import (
"path/filepath"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
"github.com/xpunch/go-micro-dashboard/config"
"github.com/xpunch/go-micro-dashboard/docs"
"github.com/xpunch/go-micro-dashboard/handler/account"
"github.com/xpunch/go-micro-dashboard/handler/registry"
"github.com/xpunch/go-micro-dashboard/handler/route"
"github.com/xpunch/go-micro-dashboard/handler/statistics"
"go-micro.dev/v4/client"
)
type Options struct {
Client client.Client
Router *gin.Engine
}
func Register(opts Options) error {
router := opts.Router
if cfg := config.GetServerConfig(); cfg.Env == config.EnvDev {
docs.SwaggerInfo.Host = cfg.Swagger.Host
docs.SwaggerInfo.BasePath = cfg.Swagger.Base
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
router.Use(static.Serve("/", static.LocalFile(config.GetServerConfig().Web.Path, false)))
router.NoRoute(func(c *gin.Context) {
c.File(filepath.Join(config.GetServerConfig().Web.Path, "index.html"))
})
if cfg := config.GetServerConfig().CORS; cfg.Enable {
router.Use(CorsHandler(cfg.Origin))
}
for _, r := range []route.Registrar{
account.NewRouteRegistrar(),
} {
r.RegisterRoute(router)
}
authRouter := opts.Router.Use(AuthRequired())
for _, r := range []route.Registrar{
registry.NewRouteRegistrar(opts.Client.Options().Registry),
statistics.NewRouteRegistrar(opts.Client.Options().Registry),
} {
r.RegisterRoute(authRouter)
}
return nil
}

@ -0,0 +1,53 @@
package handler
import (
"log"
"net/http"
"strings"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/xpunch/go-micro-dashboard/config"
)
func AuthRequired() gin.HandlerFunc {
return func(ctx *gin.Context) {
log.Println(ctx.Request)
if ctx.Request.Method == "OPTIONS" {
ctx.Next()
return
}
tokenString := ctx.GetHeader("Authorization")
if len(tokenString) == 0 || !strings.HasPrefix(tokenString, "Bearer ") {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, "")
return
}
tokenString = tokenString[7:]
claims := jwt.StandardClaims{}
token, err := jwt.ParseWithClaims(tokenString, &claims, func(t *jwt.Token) (interface{}, error) {
return []byte(config.GetAuthConfig().TokenSecret), nil
})
if err != nil {
ctx.AbortWithError(401, err)
}
if !token.Valid {
ctx.AbortWithStatus(401)
}
ctx.Set("username", claims.Subject)
ctx.Next()
}
}
func CorsHandler(allowOrigin string) gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Header("Access-Control-Allow-Origin", allowOrigin)
ctx.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
ctx.Header("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT, OPTIONS")
ctx.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
ctx.Header("Access-Control-Allow-Credentials", "true")
if ctx.Request.Method == "OPTIONS" {
ctx.AbortWithStatus(http.StatusNoContent)
}
ctx.Next()
}
}

@ -0,0 +1,32 @@
package registry
type registryService struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Endpoints []registryEndpoint `json:"endpoints,omitempty"`
Nodes []registryNode `json:"nodes,omitempty"`
}
type registryEndpoint struct {
Name string `json:"name"`
Request registryValue `json:"request"`
Response registryValue `json:"response"`
Metadata map[string]string `json:"metadata"`
}
type registryNode struct {
Id string `json:"id"`
Address string `json:"address"`
Metadata map[string]string `json:"metadata"`
}
type registryValue struct {
Name string `json:"name"`
Type string `json:"type"`
Values []registryValue `json:"values"`
}
type getServiceListResponse struct {
Services []registryService `json:"services"`
}

@ -0,0 +1,73 @@
package registry
import (
"github.com/gin-gonic/gin"
"github.com/xpunch/go-micro-dashboard/handler/route"
"go-micro.dev/v4/registry"
)
type service struct {
registry registry.Registry
}
func NewRouteRegistrar(registry registry.Registry) route.Registrar {
return service{registry: registry}
}
func (s service) RegisterRoute(router gin.IRoutes) {
router.POST("/api/registry/services", s.GetServices)
}
// @Tags Registry
// @ID registry_getServices
// @Success 200 {object} getServiceListResponse
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/registry/services [get]
func (h *service) GetServices(ctx *gin.Context) {
services, err := h.registry.ListServices()
if err != nil {
ctx.AbortWithStatusJSON(500, err)
}
var convertValue func(v *registry.Value) registryValue
convertValue = func(v *registry.Value) registryValue {
res := registryValue{
Name: v.Name,
Type: v.Type,
Values: make([]registryValue, 0, len(v.Values)),
}
for _, vv := range v.Values {
res.Values = append(res.Values, convertValue(vv))
}
return res
}
resp := getServiceListResponse{Services: make([]registryService, 0, len(services))}
for _, s := range services {
endpoints := make([]registryEndpoint, 0, len(s.Endpoints))
for _, e := range s.Endpoints {
endpoints = append(endpoints, registryEndpoint{
Name: e.Name,
Request: convertValue(e.Request),
Response: convertValue(e.Response),
Metadata: e.Metadata,
})
}
nodes := make([]registryNode, 0, len(s.Nodes))
for _, n := range s.Nodes {
nodes = append(nodes, registryNode{
Id: n.Id,
Address: n.Address,
Metadata: n.Metadata,
})
}
resp.Services = append(resp.Services, registryService{
Name: s.Name,
Version: s.Version,
Metadata: s.Metadata,
Endpoints: endpoints,
Nodes: nodes,
})
}
ctx.JSON(200, services)
}

@ -0,0 +1,7 @@
package route
import "github.com/gin-gonic/gin"
type Registrar interface {
RegisterRoute(gin.IRoutes)
}

@ -0,0 +1,16 @@
package statistics
type getSummaryResponse struct {
Registry registrySummary `json:"registry"`
Services servicesSummary `json:"services"`
}
type registrySummary struct {
Type string `json:"type"`
Addrs []string `json:"addrs"`
}
type servicesSummary struct {
Count int `json:"count"`
NodesCount int `json:"nodes_count"`
}

@ -0,0 +1,52 @@
package statistics
import (
"github.com/gin-gonic/gin"
"github.com/xpunch/go-micro-dashboard/handler/route"
"go-micro.dev/v4/registry"
)
type service struct {
registry registry.Registry
}
func NewRouteRegistrar(registry registry.Registry) route.Registrar {
return service{registry: registry}
}
func (s service) RegisterRoute(router gin.IRoutes) {
router.POST("/api/summary", s.GetSummary)
}
// @Tags Statistics
// @ID statistics_getSummary
// @Success 200 {object} getSummaryResponse
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/summary [get]
func (s *service) GetSummary(ctx *gin.Context) {
services, err := s.registry.ListServices()
if err != nil {
ctx.AbortWithStatusJSON(500, err)
}
servicesByName := make(map[string]struct{})
var servicesNodesCount int
for _, s := range services {
if _, ok := servicesByName[s.Name]; !ok {
servicesByName[s.Name] = struct{}{}
}
servicesNodesCount += len(s.Nodes)
}
var resp = getSummaryResponse{
Registry: registrySummary{
Type: s.registry.String(),
Addrs: s.registry.Options().Addrs,
},
Services: servicesSummary{
Count: len(servicesByName),
NodesCount: servicesNodesCount,
},
}
ctx.JSON(200, resp)
}

@ -0,0 +1,42 @@
package main
import (
mhttp "github.com/asim/go-micro/plugins/server/http/v4"
"github.com/gin-gonic/gin"
"github.com/xpunch/go-micro-dashboard/config"
"github.com/xpunch/go-micro-dashboard/handler"
"go-micro.dev/v4"
"go-micro.dev/v4/logger"
)
const (
Name = "go.micro.dashboard"
Version = "1.0.0"
)
func main() {
if err := config.Load(); err != nil {
logger.Fatal(err)
}
srv := micro.NewService(micro.Server(mhttp.NewServer()))
opts := []micro.Option{
micro.Name(Name),
micro.Address(config.GetServerConfig().Address),
micro.Version(Version),
}
srv.Init(opts...)
if config.GetServerConfig().Env == config.EnvProd {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(gin.Recovery(), gin.Logger())
if err := handler.Register(handler.Options{Client: srv.Client(), Router: router}); err != nil {
logger.Fatal(err)
}
if err := micro.RegisterHandler(srv.Server(), router); err != nil {
logger.Fatal(err)
}
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

@ -0,0 +1,7 @@
package main
import (
_ "github.com/asim/go-micro/plugins/client/grpc/v4"
_ "github.com/asim/go-micro/plugins/registry/etcd/v4"
_ "github.com/asim/go-micro/plugins/registry/kubernetes/v4"
)

@ -0,0 +1,7 @@
package main
// @title Go Micro Dashboard API
// @version 1.0.0
// @description This is the go micro dashboard api server.
// @termsOfService http://swagger.io/terms/
// @BasePath /

@ -0,0 +1,22 @@
package util
import (
"runtime/debug"
"go-micro.dev/v4/logger"
)
// GoSafe will run func in goroutine safely, avoid crash from unexpected panic
func GoSafe(fn func()) {
if fn == nil {
return
}
go func() {
defer func() {
if e := recover(); e != nil {
logger.Errorf("[panic]%v\n%s", e, debug.Stack())
}
}()
fn()
}()
}
Loading…
Cancel
Save