|
@@ -0,0 +1,240 @@
|
|
|
+/**
|
|
|
+@desc
|
|
|
+请您帮我设计一个ionic/angular项目的TokenGuard路由守卫,检查localStorage是否有token值。若不存在,通过dom构建ui交互提示用户填写token(不使用modal和angular逻辑)。若存在token或填写后,则调用接口:
|
|
|
+curl -X GET \
|
|
|
+ -H "X-Parse-Application-Id: ${APPLICATION_ID}" \
|
|
|
+ -H "X-Parse-Session-Token: r:pnktnjyb996sj4p156gjtp4im" \
|
|
|
+ https://YOUR.PARSE-SERVER.HERE/parse/users/me
|
|
|
+可用fetch请求验证token是否正常。
|
|
|
+若不正常,提示错误请重新填写直到填写了有效token。
|
|
|
+若正常则返回true。
|
|
|
+接口地址为https://server.fmode.cn/parse 应用id为 ncloudmaster。
|
|
|
+
|
|
|
+@example 路由守卫使用
|
|
|
+import { NgModule } from '@angular/core';
|
|
|
+import { Routes, RouterModule } from '@angular/router';
|
|
|
+import { TokenGuard } from './guards/token.guard';
|
|
|
+
|
|
|
+const routes: Routes = [
|
|
|
+ {
|
|
|
+ path: 'protected',
|
|
|
+ loadChildren: () => import('./protected/protected.module').then(m => m.ProtectedPageModule),
|
|
|
+ canActivate: [TokenGuard]
|
|
|
+ },
|
|
|
+ // 其他路由...
|
|
|
+];
|
|
|
+
|
|
|
+@NgModule({
|
|
|
+ imports: [RouterModule.forRoot(routes)],
|
|
|
+ exports: [RouterModule]
|
|
|
+})
|
|
|
+export class AppRoutingModule {}
|
|
|
+
|
|
|
+ */
|
|
|
+
|
|
|
+// src/app/guards/token.guard.ts
|
|
|
+import { Injectable } from '@angular/core';
|
|
|
+import { CanActivate, Router } from '@angular/router';
|
|
|
+import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
|
+
|
|
|
+@Injectable({
|
|
|
+ providedIn: 'root'
|
|
|
+})
|
|
|
+export class TokenGuard implements CanActivate {
|
|
|
+ private readonly PARSE_SERVER_URL = 'https://server.fmode.cn/parse';
|
|
|
+ private readonly APPLICATION_ID = 'ncloudmaster';
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private http: HttpClient,
|
|
|
+ private router: Router
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ async canActivate(): Promise<boolean> {
|
|
|
+ let token = localStorage.getItem('token');
|
|
|
+
|
|
|
+ if (!token) {
|
|
|
+ token = await this.showTokenPrompt();
|
|
|
+ if (!token) {
|
|
|
+ this.router.navigate(['/login']);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const isValid = await this.validateToken(token);
|
|
|
+ if (!isValid) {
|
|
|
+ localStorage.removeItem('token');
|
|
|
+ return this.canActivate();
|
|
|
+ }
|
|
|
+
|
|
|
+ localStorage.setItem('token', token);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async validateToken(token: string): Promise<boolean> {
|
|
|
+ const headers = new HttpHeaders({
|
|
|
+ 'X-Parse-Application-Id': this.APPLICATION_ID,
|
|
|
+ 'X-Parse-Session-Token': token
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response: any = await this.http.get(
|
|
|
+ `${this.PARSE_SERVER_URL}/users/me`,
|
|
|
+ { headers }
|
|
|
+ ).toPromise();
|
|
|
+ return !!response?.objectId;
|
|
|
+ } catch (error) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private showTokenPrompt(): Promise<string | null> {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ // 创建遮罩层
|
|
|
+ const overlay = document.createElement('div');
|
|
|
+ overlay.style.position = 'fixed';
|
|
|
+ overlay.style.top = '0';
|
|
|
+ overlay.style.left = '0';
|
|
|
+ overlay.style.width = '100%';
|
|
|
+ overlay.style.height = '100%';
|
|
|
+ overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
|
|
+ overlay.style.display = 'flex';
|
|
|
+ overlay.style.justifyContent = 'center';
|
|
|
+ overlay.style.alignItems = 'center';
|
|
|
+ overlay.style.zIndex = '1000';
|
|
|
+
|
|
|
+ // 创建对话框
|
|
|
+ const dialog = document.createElement('div');
|
|
|
+ dialog.style.backgroundColor = 'white';
|
|
|
+ dialog.style.padding = '20px';
|
|
|
+ dialog.style.borderRadius = '8px';
|
|
|
+ dialog.style.width = '80%';
|
|
|
+ dialog.style.maxWidth = '400px';
|
|
|
+
|
|
|
+ // 创建标题
|
|
|
+ const title = document.createElement('h3');
|
|
|
+ title.textContent = '请输入 Token';
|
|
|
+ title.style.marginTop = '0';
|
|
|
+ title.style.color = "black";
|
|
|
+ dialog.appendChild(title);
|
|
|
+
|
|
|
+ // 创建描述
|
|
|
+ // 使用以下指令获取token
|
|
|
+
|
|
|
+ const descEl:HTMLElement = document.createElement("div");
|
|
|
+ descEl.innerHTML = `获取token方法:<br>
|
|
|
+ 1.登录<a href="https://ai.fmode.cn" target="_blank">https://ai.fmode.cn</a><br>
|
|
|
+ 2.按F12进入调试——打开控制台Console<br>
|
|
|
+ 3.输入指令:<br>
|
|
|
+ <span style="color:blue;">JSON.parse(localStorage.getItem("Parse/ncloudmaster/currentUser"))?.sessionToken</span><br>
|
|
|
+ 4.复制字符串内容,形如:<br>
|
|
|
+ <span style="color:red">r:xxxxxxxxxxxxx</span>
|
|
|
+ `
|
|
|
+ descEl.style.color = "black"
|
|
|
+ dialog.appendChild(descEl);
|
|
|
+
|
|
|
+ // 创建错误消息容器
|
|
|
+ const errorMsg = document.createElement('div');
|
|
|
+ errorMsg.style.color = 'red';
|
|
|
+ errorMsg.style.minHeight = '20px';
|
|
|
+ errorMsg.style.margin = '10px 0';
|
|
|
+ dialog.appendChild(errorMsg);
|
|
|
+
|
|
|
+ // 创建输入框
|
|
|
+ const input = document.createElement('input');
|
|
|
+ input.type = 'text';
|
|
|
+ input.placeholder = '请输入您的 Token';
|
|
|
+ input.style.width = '100%';
|
|
|
+ input.style.padding = '10px';
|
|
|
+ input.style.marginBottom = '15px';
|
|
|
+ input.style.boxSizing = 'border-box';
|
|
|
+ dialog.appendChild(input);
|
|
|
+
|
|
|
+ // 创建按钮容器
|
|
|
+ const buttonContainer = document.createElement('div');
|
|
|
+ buttonContainer.style.display = 'flex';
|
|
|
+ buttonContainer.style.justifyContent = 'flex-end';
|
|
|
+ buttonContainer.style.gap = '10px';
|
|
|
+
|
|
|
+ // 创建提交按钮
|
|
|
+ const submitBtn = document.createElement('button');
|
|
|
+ submitBtn.textContent = '提交';
|
|
|
+ submitBtn.style.padding = '8px 16px';
|
|
|
+ submitBtn.style.backgroundColor = '#3880ff';
|
|
|
+ submitBtn.style.color = 'white';
|
|
|
+ submitBtn.style.border = 'none';
|
|
|
+ submitBtn.style.borderRadius = '4px';
|
|
|
+ submitBtn.disabled = true;
|
|
|
+
|
|
|
+ // 创建取消按钮
|
|
|
+ const cancelBtn = document.createElement('button');
|
|
|
+ cancelBtn.textContent = '取消';
|
|
|
+ cancelBtn.style.padding = '8px 16px';
|
|
|
+ cancelBtn.style.backgroundColor = '#eb445a';
|
|
|
+ cancelBtn.style.color = 'white';
|
|
|
+ cancelBtn.style.border = 'none';
|
|
|
+ cancelBtn.style.borderRadius = '4px';
|
|
|
+
|
|
|
+ // 添加按钮到容器
|
|
|
+ buttonContainer.appendChild(cancelBtn);
|
|
|
+ buttonContainer.appendChild(submitBtn);
|
|
|
+ dialog.appendChild(buttonContainer);
|
|
|
+
|
|
|
+ // 添加到遮罩层
|
|
|
+ overlay.appendChild(dialog);
|
|
|
+ document.body.appendChild(overlay);
|
|
|
+
|
|
|
+ // 自动聚焦输入框
|
|
|
+ input.focus();
|
|
|
+
|
|
|
+ // 输入验证
|
|
|
+ input.addEventListener('input', () => {
|
|
|
+ submitBtn.disabled = !input.value.trim();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 取消按钮事件
|
|
|
+ const cleanup = () => {
|
|
|
+ document.body.removeChild(overlay);
|
|
|
+ };
|
|
|
+
|
|
|
+ cancelBtn.addEventListener('click', () => {
|
|
|
+ cleanup();
|
|
|
+ resolve(null);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 提交按钮事件
|
|
|
+ submitBtn.addEventListener('click', async () => {
|
|
|
+ const token = input.value.trim();
|
|
|
+ if (!token) return;
|
|
|
+
|
|
|
+ const isValid = await this.validateToken(token);
|
|
|
+ if (isValid) {
|
|
|
+ cleanup();
|
|
|
+ resolve(token);
|
|
|
+ } else {
|
|
|
+ errorMsg.textContent = 'Token 无效,请重新输入';
|
|
|
+ input.value = '';
|
|
|
+ submitBtn.disabled = true;
|
|
|
+ input.focus();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 回车键提交
|
|
|
+ input.addEventListener('keypress', async (e) => {
|
|
|
+ if (e.key === 'Enter' && input.value.trim()) {
|
|
|
+ const token = input.value.trim();
|
|
|
+ const isValid = await this.validateToken(token);
|
|
|
+
|
|
|
+ if (isValid) {
|
|
|
+ cleanup();
|
|
|
+ resolve(token);
|
|
|
+ } else {
|
|
|
+ errorMsg.textContent = 'Token 无效,请重新输入';
|
|
|
+ input.value = '';
|
|
|
+ submitBtn.disabled = true;
|
|
|
+ input.focus();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|