Skip to content

Commit 2b4c939

Browse files
committed
feat(portal): Login as any user.
1 parent 125fa62 commit 2b4c939

14 files changed

+313
-8
lines changed

src/app/admin/admin.component.css

Whitespace-only changes.

src/app/admin/admin.component.html

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<h1 class="page-heading">管理员</h1>
2+
3+
<ng-template #pagination>
4+
<pagination-template *ngIf="!loading" #p="paginationApi" class="user-select-none" id="server" maxSize="7"
5+
style="cursor: default"
6+
(pageChange)="pageChanged($event)" (pageBoundsCorrection)="pageChanged($event)">
7+
<ul class="pagination pagination-sm justify-content-center my-2">
8+
<li class="page-item" [class.disabled]="p.isFirstPage()">
9+
<a class="page-link" (click)="p.previous()">&nbsp;<&nbsp;</a>
10+
</li>
11+
<li *ngFor="let page of p.pages" [class.active]="p.getCurrent() === page.value" class="page-item">
12+
<a *ngIf="p.getCurrent() == page.value" class="page-link">{{ page.label }}</a>
13+
<a *ngIf="p.getCurrent() != page.value" (click)="p.setCurrent(page.value)"
14+
class="page-link">{{ page.label }}</a>
15+
</li>
16+
<li class="page-item" [class.disabled]="p.isLastPage()">
17+
<a class="page-link" (click)="p.next()">&nbsp;>&nbsp;</a>
18+
</li>
19+
</ul>
20+
</pagination-template>
21+
</ng-template>
22+
23+
<div class="row mb-2 g-1">
24+
<div class="col-12 p-0">
25+
<input type="text" class="form-control form-control-sm" placeholder="pattern" [formControl]="patternControl">
26+
</div>
27+
</div>
28+
<div class="row mb-2 g-1">
29+
<div class="col-12 p-0">
30+
<button class="btn btn-primary btn-sm w-100" (click)="search()">搜索</button>
31+
</div>
32+
</div>
33+
34+
<ng-container *ngTemplateOutlet="pagination;"></ng-container>
35+
<div *ngIf="userList" class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-2">
36+
<ng-container
37+
*ngFor="let item of userList | paginate: { id: 'server', itemsPerPage: 12, currentPage: currentPage, totalItems: totalElements }; let i = index">
38+
<div class="col">
39+
<div class="card h-100 card-btn user-select-none" (click)="modalService.open(jsonModal, {centered: true, scrollable: true})">
40+
<div class="card-header fw-bold">
41+
{{ item.user.id }}.{{ item.user.username }}
42+
</div>
43+
<div class="card-body small">
44+
<div>
45+
{{ item.user.name }}
46+
</div>
47+
<div>
48+
{{ item.user.email }}
49+
</div>
50+
<div *ngFor="let oauth2 of item.user.oauth2s">
51+
{{ oauth2.email }}
52+
</div>
53+
<ng-container *ngFor="let profile of item.gameProfiles">
54+
<div *ngIf="profile.ongeki">
55+
{{ profile.ongeki.userName }}
56+
</div>
57+
<div *ngIf="profile.chusan">
58+
{{ profile.chusan.userName }}
59+
</div>
60+
<div *ngIf="profile.maimai2">
61+
{{ profile.maimai2.userName }}
62+
</div>
63+
</ng-container>
64+
</div>
65+
</div>
66+
</div>
67+
<ng-template #jsonModal let-modal>
68+
<div class="modal-header">
69+
<h5 class="modal-title">Item JSON</h5>
70+
<button type="button" class="btn-close" aria-label="Close" (click)="modal.dismiss()"></button>
71+
</div>
72+
<div class="modal-body">
73+
<button class="btn btn-primary btn-sm mb-2" (click)="loginAs(item.user.username)">
74+
夺舍
75+
</button>
76+
<pre class="small">{{ item | json }}</pre>
77+
</div>
78+
</ng-template>
79+
</ng-container>
80+
</div>
81+
<ng-container *ngTemplateOutlet="pagination;"></ng-container>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { AdminComponent } from './admin.component';
4+
5+
describe('AdminComponent', () => {
6+
let component: AdminComponent;
7+
let fixture: ComponentFixture<AdminComponent>;
8+
9+
beforeEach(() => {
10+
TestBed.configureTestingModule({
11+
declarations: [AdminComponent]
12+
});
13+
fixture = TestBed.createComponent(AdminComponent);
14+
component = fixture.componentInstance;
15+
fixture.detectChanges();
16+
});
17+
18+
it('should create', () => {
19+
expect(component).toBeTruthy();
20+
});
21+
});

src/app/admin/admin.component.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import {Component, OnInit} from '@angular/core';
2+
import {FormControl} from '@angular/forms';
3+
import {combineLatest, ReplaySubject, startWith, tap} from 'rxjs';
4+
import {ActivatedRoute} from '@angular/router';
5+
import {ApiService} from '../api.service';
6+
import {MessageService} from '../message.service';
7+
import {Card, User} from '../user.service';
8+
import {StatusCode} from '../status-code';
9+
import {V2Profile} from '../sega/chunithm/v2/model/V2Profile';
10+
import {DisplayOngekiProfile} from '../sega/ongeki/model/OngekiProfile';
11+
import {DisplayMaimai2Profile} from '../sega/maimai2/model/Maimai2Profile';
12+
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
13+
import {AuthenticationService} from '../auth/authentication.service';
14+
import {TranslateService} from '@ngx-translate/core';
15+
16+
@Component({
17+
selector: 'app-admin',
18+
templateUrl: './admin.component.html',
19+
styleUrls: ['./admin.component.css']
20+
})
21+
export class AdminComponent implements OnInit {
22+
private pageSubject = new ReplaySubject<number>();
23+
currentPage = 1;
24+
totalElements = 0;
25+
loading = true;
26+
27+
patternControl = new FormControl('');
28+
29+
userList: AdvancedUser[];
30+
31+
constructor(
32+
private api: ApiService,
33+
private messageService: MessageService,
34+
private route: ActivatedRoute,
35+
private translate: TranslateService,
36+
private authenticationService: AuthenticationService,
37+
protected modalService: NgbModal
38+
) {
39+
40+
}
41+
42+
ngOnInit(): void {
43+
combineLatest([
44+
this.pageSubject.pipe(startWith(0)),
45+
]).subscribe(([page]) => {
46+
this.load(page, this.patternControl.value);
47+
});
48+
}
49+
50+
load(page: number, pattern: string) {
51+
this.currentPage = page + 1;
52+
const params: any = {page, size: 12};
53+
if (pattern !== '') {
54+
params.pattern = pattern;
55+
}
56+
this.api.get('api/admin/advancedUserSearch', params).subscribe({
57+
next: resp => {
58+
const statusCode: StatusCode = resp.status.code;
59+
if (statusCode === StatusCode.OK && resp.data) {
60+
this.userList = resp.data.content;
61+
this.totalElements = resp.data.totalElements;
62+
}
63+
else{
64+
this.messageService.notice(resp.status.message, 'warning');
65+
}
66+
this.loading = false;
67+
},
68+
error: err => {
69+
this.messageService.notice(err.message, 'warning');
70+
this.loading = false;
71+
}
72+
});
73+
}
74+
75+
search() {
76+
this.load(0, this.patternControl.value);
77+
}
78+
79+
pageChanged(page: number) {
80+
this.pageSubject.next(page - 1);
81+
}
82+
83+
loginAs(username: string){
84+
this.authenticationService.loginAs(username)
85+
.subscribe(
86+
{
87+
next: (resp) => {
88+
if (resp?.status) {
89+
const statusCode: StatusCode = resp.status.code;
90+
if (statusCode === StatusCode.OK && resp.data) {
91+
this.messageService.notice(resp.status.message);
92+
location.reload();
93+
}
94+
else if (statusCode === StatusCode.LOGIN_FAILED){
95+
this.translate.get('SignInPage.LoginFailedMessage').subscribe((res: string) => {
96+
this.messageService.notice(res, 'danger');
97+
});
98+
}
99+
else{
100+
this.messageService.notice(resp.status.message);
101+
}
102+
}
103+
},
104+
error: (error) => {
105+
this.messageService.notice(error);
106+
console.warn('login fail', error);
107+
}
108+
}
109+
);
110+
}
111+
}
112+
113+
export interface AdvancedUser{
114+
user: User;
115+
gameProfiles: GameProfile[];
116+
}
117+
118+
export interface GameProfile{
119+
chusan: V2Profile;
120+
ongeki: DisplayOngekiProfile;
121+
maimai2: DisplayMaimai2Profile;
122+
card: Card;
123+
}

src/app/app-routing.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {PasswordResetComponent} from './password-reset/password-reset.component'
1616
import {ProfileComponent} from './profile/profile.component';
1717
import {AnnouncementsComponent} from './announcements/announcements.component';
1818
import {EditComponent} from "./announcements/edit/edit.component";
19+
import {AdminComponent} from './admin/admin.component';
1920

2021
const routes: Routes = [
2122
{path: '', component: HomeComponent, data: {title: 'Home', disableSidebar: true }},
@@ -34,6 +35,7 @@ const routes: Routes = [
3435
{path: 'sign-in', component: SignInComponent, canActivate: [LoginGuardService], data: {title: 'SignIn', disableSidebar: true }},
3536
{path: 'sign-up', component: SignUpComponent, canActivate: [LoginGuardService], data: {title: 'SignUp', disableSidebar: true }},
3637
{path: 'password-reset', component: PasswordResetComponent, canActivate: [LoginGuardService], data: {title: 'ResetPassword', disableSidebar: true }},
38+
{path: 'admin', component: AdminComponent, canActivate: [AuthGuardService], data: {title: 'Admin'}},
3739
{path: 'not-found', component: NotFoundComponent, data: {title: 'NotFound', disableSidebar: true }},
3840
{path: '**', redirectTo: '/not-found'}
3941
];

src/app/app.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ <h5 class="offcanvas-title" id="offcanvasExampleLabel">{{'App.Sidebar.Navigation
7070
<li class="pb-2">
7171
<a class="link-btn rounded" [class.active]="isActive('/import')" (click)="navigateTo('/import')">{{'App.Sidebar.Import' | translate}}</a>
7272
</li>
73+
<li *ngIf="isAdmin()" class="pb-2">
74+
<a class="link-btn rounded" [class.active]="isActive('/admin')" (click)="navigateTo('/admin')">{{'App.Sidebar.Admin' | translate}}</a>
75+
</li>
7376
<!-- <li class="pb-2">-->
7477
<!-- <a class="link-btn rounded" target="_blank"-->
7578
<!-- href="https://status.naominet.live/status/aquaserver">{{'App.Sidebar.Status' | translate}}</a>-->

src/app/app.component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export class AppComponent implements OnInit, OnDestroy {
5353
protected themeService: ThemeService,
5454
protected messageService: MessageService,
5555
protected translateService: TranslateService,
56+
protected user: UserService,
5657
updates: SwUpdate,
5758
@Inject(DOCUMENT) private document: Document,
5859
) {
@@ -174,6 +175,10 @@ export class AppComponent implements OnInit, OnDestroy {
174175
this.hideSidebar();
175176
}
176177

178+
isAdmin() {
179+
return this.user.currentUser?.roles?.some(r => r.id === 5) ?? false;
180+
}
181+
177182
@HostListener('window:popstate', ['$event'])
178183
onPopState(event: any) {
179184
if (this.sidebarOffcanvasOpened) {

src/app/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { PasswordResetComponent } from './password-reset/password-reset.componen
6969
import { ProfileComponent } from './profile/profile.component';
7070
import { AnnouncementsComponent } from './announcements/announcements.component';
7171
import { EditComponent } from './announcements/edit/edit.component';
72+
import { AdminComponent } from './admin/admin.component';
7273

7374
const aegis = new Aegis({
7475
id: 'j4KOYFL0VyajP4KjdG', // 上报 id
@@ -106,7 +107,8 @@ export function initializeApp(
106107
SignInComponent,
107108
ProfileComponent,
108109
AnnouncementsComponent,
109-
EditComponent
110+
EditComponent,
111+
AdminComponent
110112
],
111113
imports: [
112114
BrowserModule,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { AdminGuardService } from './admin-guard.service';
4+
5+
describe('AdminGuardService', () => {
6+
let service: AdminGuardService;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(AdminGuardService);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {Injectable} from '@angular/core';
2+
import {ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot} from '@angular/router';
3+
import {AccountService} from './account.service';
4+
import {UserService} from '../user.service';
5+
6+
@Injectable({
7+
providedIn: 'root'
8+
})
9+
export class AdminGuardService implements CanLoad, CanActivate {
10+
11+
constructor(
12+
private router: Router,
13+
protected user: UserService) {
14+
}
15+
16+
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
17+
if (this.isAdmin()) {
18+
return true;
19+
}
20+
21+
this.router.navigate(['/dashboard']);
22+
return false;
23+
}
24+
25+
canLoad(route: Route) {
26+
if (this.isAdmin()) {
27+
return true;
28+
}
29+
30+
this.router.navigate(['/dashboard']);
31+
return false;
32+
}
33+
34+
isAdmin() {
35+
return this.user.currentUser?.roles?.some(r => r.id === 5) ?? false;
36+
}
37+
}

0 commit comments

Comments
 (0)