Files
ftb-project-management/apps/web/lib/members.ts
2026-07-02 09:30:50 +08:00

130 lines
3.6 KiB
TypeScript

import { pinyin } from 'pinyin-pro';
export interface Department {
id: string;
name: string;
parentId?: string;
order: number;
createdAt: string;
}
export interface Member {
id: string;
name: string;
username?: string;
departmentId: string;
roleId: string;
phone: string;
email: string;
password: string;
createdAt: string;
isSystem?: boolean;
}
export interface RoleItem {
id: string;
name: string;
description?: string;
createdAt: string;
isSystem?: boolean;
permissions: string[];
}
export interface PasswordRule {
prefix: string;
length: number;
includeUppercase: boolean;
includeLowercase: boolean;
includeNumbers: boolean;
includeSpecial: boolean;
}
export const DEFAULT_PASSWORD_RULE: PasswordRule = {
prefix: 'Ftb',
length: 8,
includeUppercase: true,
includeLowercase: true,
includeNumbers: true,
includeSpecial: false,
};
export function generatePassword(rule: PasswordRule): string {
let chars = '';
if (rule.includeUppercase) chars += 'ABCDEFGHJKLMNPQRSTUVWXYZ';
if (rule.includeLowercase) chars += 'abcdefghjkmnpqrstuvwxyz';
if (rule.includeNumbers) chars += '23456789';
if (rule.includeSpecial) chars += '!@#$%&*';
if (!chars) chars = 'abcdefghjkmnpqrstuvwxyz23456789';
const randomLength = Math.max(rule.length - rule.prefix.length, 4);
let result = rule.prefix;
for (let i = 0; i < randomLength; i++) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}
export function maskPhone(phone: string): string {
if (!phone || phone.length < 7) return phone;
return phone.slice(0, 3) + '****' + phone.slice(7);
}
export function normalizeUsername(username: string | undefined | null): string {
return (username ?? '').trim().replace(/\s+/g, '').toLowerCase();
}
function nameToUsernameBase(name: string): string {
const raw = name.trim();
if (!raw) return 'USER';
const letters = pinyin(raw, { toneType: 'none', type: 'array' })
.join('')
.replace(/[^a-zA-Z0-9]/g, '')
.toLowerCase();
return letters || 'USER';
}
function duplicateSuffix(attempt: number): string {
return attempt === 1 ? '1' : `${'0'.repeat(attempt - 1)}1`;
}
function uniqueUsernameFromBase(base: string, existingUsernames: Iterable<string>): string {
const normalizedBase = normalizeUsername(base) || 'USER';
const used = new Set(Array.from(existingUsernames, normalizeUsername).filter(Boolean));
if (!used.has(normalizedBase)) return normalizedBase;
for (let attempt = 1; attempt < 1000; attempt += 1) {
const candidate = `${normalizedBase}${duplicateSuffix(attempt)}`;
if (!used.has(candidate)) return candidate;
}
return `${normalizedBase}${Date.now()}`;
}
export function generateUsernameFromName(
name: string,
existingUsernames: Iterable<string>,
currentUsername?: string,
): string {
const normalizedCurrent = normalizeUsername(currentUsername);
if (normalizedCurrent) return normalizedCurrent;
return uniqueUsernameFromBase(nameToUsernameBase(name), existingUsernames);
}
export function ensureMemberUsernames(members: Member[]): { members: Member[]; changed: boolean } {
const used = new Set<string>();
let changed = false;
const normalizedMembers = members.map((member) => {
const requested = normalizeUsername(member.username);
const username = requested
? uniqueUsernameFromBase(requested, used)
: generateUsernameFromName(member.name, used);
used.add(username);
if (member.username === username) return member;
changed = true;
return { ...member, username };
});
return { members: normalizedMembers, changed };
}