Explorar el Código

feat(auth): align member login and managed passwords

彭峰 hace 15 horas
padre
commit
dccc2d2d28

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 1
projects/xiaoshu-admin/src/app/pages/operations-user-detail.component.html


+ 4 - 2
projects/xiaoshu-mobile/src/app/core/auth.guard.ts

@@ -5,7 +5,8 @@ import { SessionService } from './session.service';
 export const authGuard: CanActivateFn = (_route, state) => {
   const session = inject(SessionService);
   const router = inject(Router);
-  if (session.isLoggedIn() && session.user()?.['passwordResetRequired'] === true && !state.url.startsWith('/pages/member/passwd')&&!state.url.startsWith('/pages/member/acc_destory')) return router.createUrlTree(['/pages/member/passwd']);
+  const user=session.user(),member=Number(user?.groupId)===1||user?.['identityType']==='member';
+  if (session.isLoggedIn() && !member && user?.['passwordResetRequired'] === true && !state.url.startsWith('/pages/member/passwd')&&!state.url.startsWith('/pages/member/acc_destory')) return router.createUrlTree(['/pages/member/passwd']);
   return session.isLoggedIn()
     ? true
     : router.createUrlTree(['/pages/member/wxauth'], { queryParams: { returnUrl: state.url } });
@@ -13,5 +14,6 @@ export const authGuard: CanActivateFn = (_route, state) => {
 
 export const passwordResetGuard:CanActivateFn=(_route,state)=>{
  const session=inject(SessionService),router=inject(Router);
- return session.isLoggedIn()&&session.user()?.['passwordResetRequired']===true&&!['/pages/member/passwd','/pages/member/wxauth','/pages/member/acc_destory','/pages/home/enjoy_item'].some(path=>state.url.startsWith(path))?router.createUrlTree(['/pages/member/passwd']):true;
+ const user=session.user(),member=Number(user?.groupId)===1||user?.['identityType']==='member';
+ return session.isLoggedIn()&&!member&&user?.['passwordResetRequired']===true&&!['/pages/member/passwd','/pages/member/wxauth','/pages/member/acc_destory','/pages/home/enjoy_item'].some(path=>state.url.startsWith(path))?router.createUrlTree(['/pages/member/passwd']):true;
 };

+ 1 - 1
projects/xiaoshu-mobile/src/app/features/auth/login-page.component.html

@@ -16,7 +16,7 @@
       <label class="field-label" for="login-mobile">手机号码</label><div class="input-wrap"><lucide-icon [img]="icons.Phone" [size]="19" /><input aria-label="手机号码" id="login-mobile" name="mobile" type="tel" inputmode="numeric" autocomplete="tel" maxlength="11" [(ngModel)]="mobile" placeholder="11位手机号码" /></div>
       <label class="field-label" for="login-code">短信验证码</label><div class="input-wrap code"><lucide-icon [img]="icons.MessageSquareText" [size]="19" /><input aria-label="短信验证码" id="login-code" name="code" inputmode="numeric" maxlength="6" [(ngModel)]="code" placeholder="6位短信验证码" /><button type="button" (click)="sendCode()" [disabled]="countdown() > 0">{{ countdown() ? countdown() + '秒' : '获取验证码' }}</button></div>
     } @else {
-      <label class="field-label" for="login-account">账号</label><div class="input-wrap"><lucide-icon [img]="icons.Phone" [size]="19" /><input aria-label="账号" id="login-account" name="account" autocomplete="username" [(ngModel)]="account" placeholder="手机号码或账号" /></div>
+      <label class="field-label" for="login-account">账号</label><div class="input-wrap"><lucide-icon [img]="icons.Phone" [size]="19" /><input aria-label="账号" id="login-account" name="account" autocomplete="username" [(ngModel)]="account" placeholder="手机号、会员姓名或账号" /></div>
       <label class="field-label" for="login-password">密码</label><div class="input-wrap"><lucide-icon [img]="icons.LockKeyhole" [size]="19" /><input aria-label="密码" id="login-password" name="password" [type]="showPassword() ? 'text' : 'password'" autocomplete="current-password" [(ngModel)]="password" [placeholder]="registering() ? '8至72位密码' : '请输入密码'" /><button class="eye-button" type="button" aria-label="显示或隐藏密码" title="显示或隐藏密码" (click)="showPassword.set(!showPassword())"><lucide-icon [img]="showPassword() ? icons.EyeOff : icons.Eye" [size]="18" /></button></div>
       @if (registering()) { <label class="field-label" for="login-confirmPassword">确认密码</label><div class="input-wrap"><lucide-icon [img]="icons.LockKeyhole" [size]="19" /><input aria-label="确认密码" id="login-confirmPassword" name="confirmPassword" type="password" autocomplete="new-password" [(ngModel)]="confirmPassword" placeholder="再次输入密码" /></div> }
     }

+ 1 - 1
projects/xiaoshu-mobile/src/app/features/auth/login-page.component.ts

@@ -148,7 +148,7 @@ export class LoginPageComponent implements OnInit, OnDestroy {
       this.platform.toast('登录状态无效,请重新登录', 'error');
       return;
     }
-    if (user['passwordResetRequired'] === true) { this.platform.loading.set(false); void this.router.navigateByUrl('/pages/member/passwd'); return; }
+    if (user['passwordResetRequired'] === true && Number(user.groupId) !== 1 && user['identityType'] !== 'member') { this.platform.loading.set(false); void this.router.navigateByUrl('/pages/member/passwd'); return; }
     const enter = (synced: boolean) => {
       this.platform.toast(synced ? (registered ? '注册成功' : '登录成功') : '登录成功,数据同步暂不可用,请稍后手动刷新', synced ? 'success' : 'error');
       const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl');

+ 5 - 4
projects/xiaoshu-mobile/src/app/features/auth/password-page.component.ts

@@ -1,14 +1,15 @@
-import {Component, inject, signal} from '@angular/core';
+import {Component, computed, inject, signal} from '@angular/core';
 import {FormsModule} from '@angular/forms';
 import {Router} from '@angular/router';
 import {finalize} from 'rxjs';
 import {ApiService} from '../../core/api.service';
 import {SessionService} from '../../core/session.service';
 @Component({selector:'app-password-page',standalone:true,imports:[FormsModule],template:`
- <main><h1>修改登录密码</h1><p>{{sessions.user()?.['passwordResetRequired'] ? '请先将临时密码改为自己的密码,再继续使用。' : '修改成功后,需要重新登录。'}}</p>
- <form (ngSubmit)="save()"><fieldset [disabled]="saving()"><label>当前密码<input name="old" type="password" autocomplete="current-password" [(ngModel)]="oldPassword" required></label><label>新密码<input name="new" type="password" autocomplete="new-password" [(ngModel)]="newPassword" minlength="8" maxlength="72" required></label><label>确认新密码<input name="confirm" type="password" autocomplete="new-password" [(ngModel)]="confirmation" required></label><p>新密码为 8 至 72 位。</p>@if(error()){<p role="alert">{{error()}}</p>}<button class="primary-button" type="submit">{{saving()?'正在保存…':'保存并重新登录'}}</button></fieldset></form></main>`,styles:[`main{max-width:520px;margin:24px auto;padding:20px}fieldset{border:0;padding:0;display:grid;gap:16px}label{display:grid;gap:8px}input,button{min-height:44px}input{border:1px solid #cadbd6;border-radius:10px;padding:10px}p[role=alert]{color:#b42318}`]})
+ <main><h1>修改登录密码</h1>@if(isMember()){<p>学生登录密码由管理员统一管理。如需重置,请联系管理员。</p>}@else{<p>{{sessions.user()?.['passwordResetRequired'] ? '请先将临时密码改为自己的密码,再继续使用。' : '修改成功后,需要重新登录。'}}</p>
+ <form (ngSubmit)="save()"><fieldset [disabled]="saving()"><label>当前密码<input name="old" type="password" autocomplete="current-password" [(ngModel)]="oldPassword" required></label><label>新密码<input name="new" type="password" autocomplete="new-password" [(ngModel)]="newPassword" minlength="8" maxlength="72" required></label><label>确认新密码<input name="confirm" type="password" autocomplete="new-password" [(ngModel)]="confirmation" required></label><p>新密码为 8 至 72 位。</p>@if(error()){<p role="alert">{{error()}}</p>}<button class="primary-button" type="submit">{{saving()?'正在保存…':'保存并重新登录'}}</button></fieldset></form>}</main>`,styles:[`main{max-width:520px;margin:24px auto;padding:20px}fieldset{border:0;padding:0;display:grid;gap:16px}label{display:grid;gap:8px}input,button{min-height:44px}input{border:1px solid #cadbd6;border-radius:10px;padding:10px}p[role=alert]{color:#b42318}`]})
 export class PasswordPageComponent {
  readonly sessions=inject(SessionService);private readonly api=inject(ApiService);private readonly router=inject(Router);
+ readonly isMember=computed(()=>Number(this.sessions.user()?.groupId)===1||this.sessions.user()?.['identityType']==='member');
  oldPassword='';newPassword='';confirmation='';readonly saving=signal(false);readonly error=signal('');
- save(){if(this.saving())return;this.error.set('');if(!this.oldPassword||this.newPassword.length<8||this.newPassword.length>72||this.newPassword!==this.confirmation){this.error.set('请填写当前密码及一致的 8 至 72 位新密码');return;}this.saving.set(true);this.api.post('app_password_change',{currentPassword:this.oldPassword,newPassword:this.newPassword}).pipe(finalize(()=>this.saving.set(false))).subscribe({next:()=>{this.oldPassword=this.newPassword=this.confirmation='';this.sessions.logout(false);void this.router.navigate(['/pages/member/wxauth'],{queryParams:{passwordChanged:1}});},error:e=>this.error.set(e.message)});}
+ save(){if(this.saving())return;this.error.set('');if(this.isMember()){this.error.set('学生密码由管理员统一管理');return;}if(!this.oldPassword||this.newPassword.length<8||this.newPassword.length>72||this.newPassword!==this.confirmation){this.error.set('请填写当前密码及一致的 8 至 72 位新密码');return;}this.saving.set(true);this.api.post('app_password_change',{currentPassword:this.oldPassword,newPassword:this.newPassword}).pipe(finalize(()=>this.saving.set(false))).subscribe({next:()=>{this.oldPassword=this.newPassword=this.confirmation='';this.sessions.logout(false);void this.router.navigate(['/pages/member/wxauth'],{queryParams:{passwordChanged:1}});},error:e=>this.error.set(e.message)});}
 }

+ 2 - 2
projects/xiaoshu-mobile/src/app/features/english-reading/reading-setup.component.html

@@ -9,8 +9,8 @@
     <section class="form-section">
       <h3>学生信息</h3>
       <div class="form-grid">
-        <label class="field"><span>学生姓名</span><div class="inline-field"><input name="nickname" [(ngModel)]="nickname" (input)="markNameDirty()" placeholder="请输入学生姓名" />@if (nameDirty()) { <button type="button" (click)="saveProfile()">保存</button> }</div></label>
-        <label class="field"><span>联系电话</span><input name="mobile" type="tel" [(ngModel)]="mobile" [disabled]="!!cloud.user?.mobile" placeholder="请输入联系电话" /></label>
+        <label class="field"><span>学习昵称(可修改)</span><div class="inline-field"><input name="nickname" [(ngModel)]="nickname" (input)="markNameDirty()" placeholder="请输入学习昵称" />@if (nameDirty()) { <button type="button" (click)="saveProfile()">保存</button> }</div></label>
+        <label class="field"><span>联系电话</span><input name="mobile" type="tel" [(ngModel)]="mobile" [disabled]="!!cloud.user?.mobile" placeholder="请输入联系电话" />@if (cloud.user?.mobile) { <small>该号码为账号绑定手机,如需更正请联系管理员。</small> }</label>
       </div>
     </section>
 

+ 2 - 1
scripts/cloud/account-security.js

@@ -5,7 +5,8 @@ async function migrateLegacyLogin(username,password) {
  const original=await migratedLoginTarget(username);
  if(!mayMigrateLegacyPassword(original)) fail(401,'账号或密码错误;如首次登录旧账号,请联系管理员重置密码');
  const revision=Number(original.get('credentialRevision')||0);
- const verified=await verifyLegacyPassword(username,password),target=await migratedLoginTarget(username,verified);
+ const legacyLoginName=String(original.get('username')||username);
+ const verified=await verifyLegacyPassword(legacyLoginName,password),target=await migratedLoginTarget(username,verified);
  if(!target || target.id!==original.id || !mayMigrateLegacyPassword(target)) fail(401,'账号验证未通过');
  target.set('credentialExpectedRevision',revision);target.set('credentialMigrationAttempt',true);
  target.setPassword(password);await target.save(null,{useMasterKey:true});

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 31 - 5
scripts/deploy-admin-functions.mjs


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio