feat(auth): rename external ids to asid psid

This commit is contained in:
2026-07-10 11:54:10 +08:00
parent 8312d08653
commit e421192560
11 changed files with 325 additions and 81 deletions
@@ -13,8 +13,8 @@ const log = new Logger("ExternalEntryPersist");
interface ExternalEntryPersistProps {
deviceId: string | null;
facebookId: string | null;
facebookAppId: string | null;
asid: string | null;
psid: string | null;
avatarUrl: string | null;
target: string | null;
redirect: string | null;
@@ -23,8 +23,8 @@ interface ExternalEntryPersistProps {
export default function ExternalEntryPersist({
deviceId,
facebookId,
facebookAppId,
asid,
psid,
avatarUrl,
target,
redirect,
@@ -43,8 +43,8 @@ export default function ExternalEntryPersist({
try {
await persistExternalEntryPayload({
deviceId,
facebookId,
facebookAppId,
asid,
psid,
avatarUrl,
});
} catch (error) {
@@ -55,10 +55,10 @@ export default function ExternalEntryPersist({
})();
}, [
avatarUrl,
asid,
deviceId,
facebookAppId,
facebookId,
next,
psid,
redirect,
router,
target,
+3 -15
View File
@@ -2,7 +2,7 @@
* 外部入口落地页(Server Component
*
* 外部应用可以通过 query 传入 Facebook 相关信息和最终目标页,例如:
* `/external-entry?target=chat&fbid=xxx&fb_app_id=yyy`
* `/external-entry?target=chat&asid=xxx&psid=yyy`
*
* 页面不直接 `redirect()`,而是把数据交给 Client 组件先写入本地存储,
* 再通过 `router.replace()` 清理 URL 并跳转到最终页面。
@@ -24,20 +24,8 @@ export default async function ExternalEntryPage({
return (
<ExternalEntryPersist
deviceId={pickFirst(params.deviceId, params.device_id)}
facebookId={pickFirst(
params.fbid,
params.fb_id,
params.facebookId,
params.facebook_id,
)}
facebookAppId={pickFirst(
params.fbAppId,
params.fb_app_id,
params.facebookAppId,
params.facebook_app_id,
params.appId,
params.app_id,
)}
asid={pickFirst(params.asid)}
psid={pickFirst(params.psid)}
avatarUrl={pickFirst(params.avatarUrl, params.avatar_url)}
target={pickFirst(params.target)}
redirect={pickFirst(params.redirect)}
+1 -1
View File
@@ -4,7 +4,7 @@
* 在 OAuth callback 之后、auth state machine 的 `syncingFacebookBackend` actor
* 成功换取业务 token 之后调用 —— 用 accessToken 调 `/me` 拉用户资料(id / name
* / email / picture),把 pictureUrl 写入 `UserStorage.setAvatarUrl()`、
* id 写入 `AuthStorage.setFacebookId()`。
* id 写入 `AuthStorage.setAsid()`。
*
* 端点:`https://graph.facebook.com/me?fields=id,name,email,picture.type(large)&access_token=...`
* - `picture.type(large)` 返回 ~200px 头像 URL(默认 square 50px 太小)
+41 -19
View File
@@ -89,28 +89,32 @@ export class AuthStorage implements IAuthStorage {
return SpAsyncUtil.remove(StorageKeys.refreshToken);
}
// ---- facebook id ----
// ---- ASID / PSID ----
getFacebookId(): Promise<ResultT<string | null>> {
return SpAsyncUtil.getString(StorageKeys.facebookId);
async getAsid(): Promise<ResultT<string | null>> {
return getStringWithLegacyFallback(
StorageKeys.asid,
StorageKeys.legacyFacebookId,
);
}
setFacebookId(id: string): Promise<ResultT<void>> {
return SpAsyncUtil.setString(StorageKeys.facebookId, id);
setAsid(id: string): Promise<ResultT<void>> {
return SpAsyncUtil.setString(StorageKeys.asid, id);
}
clearFacebookId(): Promise<ResultT<void>> {
return SpAsyncUtil.remove(StorageKeys.facebookId);
clearAsid(): Promise<ResultT<void>> {
return removeKeys(StorageKeys.asid, StorageKeys.legacyFacebookId);
}
// ---- facebook app id ----
getFacebookAppId(): Promise<ResultT<string | null>> {
return SpAsyncUtil.getString(StorageKeys.facebookAppId);
async getPsid(): Promise<ResultT<string | null>> {
return getStringWithLegacyFallback(
StorageKeys.psid,
StorageKeys.legacyFacebookAppId,
);
}
setFacebookAppId(id: string): Promise<ResultT<void>> {
return SpAsyncUtil.setString(StorageKeys.facebookAppId, id);
setPsid(id: string): Promise<ResultT<void>> {
return SpAsyncUtil.setString(StorageKeys.psid, id);
}
clearFacebookAppId(): Promise<ResultT<void>> {
return SpAsyncUtil.remove(StorageKeys.facebookAppId);
clearPsid(): Promise<ResultT<void>> {
return removeKeys(StorageKeys.psid, StorageKeys.legacyFacebookAppId);
}
// ---- bulk clear ----
@@ -120,9 +124,9 @@ export class AuthStorage implements IAuthStorage {
if (!r1.success) return r1;
const r2 = await this.clearRefreshToken();
if (!r2.success) return r2;
const r3 = await this.clearFacebookId();
const r3 = await this.clearAsid();
if (!r3.success) return r3;
const r4 = await this.clearFacebookAppId();
const r4 = await this.clearPsid();
if (!r4.success) return r4;
return this.clearLoginProvider();
}
@@ -136,12 +140,30 @@ export class AuthStorage implements IAuthStorage {
if (!r3.success) return r3;
const r4 = await this.clearRefreshToken();
if (!r4.success) return r4;
const r5 = await this.clearFacebookId();
const r5 = await this.clearAsid();
if (!r5.success) return r5;
return this.clearFacebookAppId();
return this.clearPsid();
}
}
async function getStringWithLegacyFallback(
key: string,
legacyKey: string,
): Promise<ResultT<string | null>> {
const value = await SpAsyncUtil.getString(key);
if (!value.success) return value;
if (value.data !== null && value.data.length > 0) return value;
return SpAsyncUtil.getString(legacyKey);
}
async function removeKeys(...keys: string[]): Promise<ResultT<void>> {
for (const key of keys) {
const result = await SpAsyncUtil.remove(key);
if (!result.success) return result;
}
return Result.ok(undefined);
}
function isLoginStatus(value: string): value is LoginStatusT {
return Object.values(LoginStatus).includes(value as LoginStatusT);
}
+7 -7
View File
@@ -4,7 +4,7 @@
* IAuthStorage 接口
*
* 对齐 Dart 端 `IAuthStorage`lib/data/services/storage/iauth_storage.dart):
* - loginToken / loginProvider / guestToken / deviceId / refreshToken / facebookId / facebookAppId 字段的 CRUD
* - loginToken / loginProvider / guestToken / deviceId / refreshToken / ASID / PSID 字段的 CRUD
* - `clearAuthData()` 批量清空登录态
* - `clearBusinessAuthData()` 仅清空真实用户登录态,保留 guest token / deviceId
* - 所有方法返回 `Promise<Result<T>>`,与 Dart `Future<Result<T>>` 对齐
@@ -35,13 +35,13 @@ export interface IAuthStorage {
setRefreshToken(token: string): Promise<Result<void>>;
clearRefreshToken(): Promise<Result<void>>;
getFacebookId(): Promise<Result<string | null>>;
setFacebookId(id: string): Promise<Result<void>>;
clearFacebookId(): Promise<Result<void>>;
getAsid(): Promise<Result<string | null>>;
setAsid(id: string): Promise<Result<void>>;
clearAsid(): Promise<Result<void>>;
getFacebookAppId(): Promise<Result<string | null>>;
setFacebookAppId(id: string): Promise<Result<void>>;
clearFacebookAppId(): Promise<Result<void>>;
getPsid(): Promise<Result<string | null>>;
setPsid(id: string): Promise<Result<void>>;
clearPsid(): Promise<Result<void>>;
clearBusinessAuthData(): Promise<Result<void>>;
clearAuthData(): Promise<Result<void>>;
+4 -2
View File
@@ -13,8 +13,10 @@ export const StorageKeys = {
guestToken: "guest_token",
deviceId: "device_id",
refreshToken: "refresh_token",
facebookId: "facebook_id",
facebookAppId: "facebook_app_id",
asid: "asid",
psid: "psid",
legacyFacebookId: "facebook_id",
legacyFacebookAppId: "facebook_app_id",
// user
user: "user",
+6 -6
View File
@@ -32,12 +32,12 @@ function makeAuthStorageMock(input: {
),
setRefreshToken: vi.fn(async () => Result.ok(undefined)),
clearRefreshToken: vi.fn(async () => Result.ok(undefined)),
getFacebookId: vi.fn(async () => Result.ok(null)),
setFacebookId: vi.fn(async () => Result.ok(undefined)),
clearFacebookId: vi.fn(async () => Result.ok(undefined)),
getFacebookAppId: vi.fn(async () => Result.ok(null)),
setFacebookAppId: vi.fn(async () => Result.ok(undefined)),
clearFacebookAppId: vi.fn(async () => Result.ok(undefined)),
getAsid: vi.fn(async () => Result.ok(null)),
setAsid: vi.fn(async () => Result.ok(undefined)),
clearAsid: vi.fn(async () => Result.ok(undefined)),
getPsid: vi.fn(async () => Result.ok(null)),
setPsid: vi.fn(async () => Result.ok(undefined)),
clearPsid: vi.fn(async () => Result.ok(undefined)),
clearBusinessAuthData: vi.fn(async () => Result.ok(undefined)),
clearAuthData: vi.fn(async () => Result.ok(undefined)),
};
+8 -8
View File
@@ -8,26 +8,26 @@ import { UrlLauncherUtil } from "@/utils";
export async function openChatInExternalBrowser(): Promise<void> {
const authStorage = AuthStorage.getInstance();
const userStorage = UserStorage.getInstance();
const [deviceIdR, facebookIdR, facebookAppIdR, avatarUrlR] =
const [deviceIdR, asidR, psidR, avatarUrlR] =
await Promise.all([
authStorage.getDeviceId(),
authStorage.getFacebookId(),
authStorage.getFacebookAppId(),
authStorage.getAsid(),
authStorage.getPsid(),
userStorage.getAvatarUrl(),
]);
const deviceId = deviceIdR.success ? deviceIdR.data : null;
const facebookId = facebookIdR.success ? facebookIdR.data : null;
const facebookAppId = facebookAppIdR.success ? facebookAppIdR.data : null;
const asid = asidR.success ? asidR.data : null;
const psid = psidR.success ? psidR.data : null;
const avatarUrl = avatarUrlR.success ? avatarUrlR.data : null;
if (deviceId && facebookId) {
if (deviceId && asid) {
UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.externalEntry, {
queryParams: {
target: "chat",
deviceId,
fbid: facebookId,
...(facebookAppId ? { fb_app_id: facebookAppId } : {}),
asid,
...(psid ? { psid } : {}),
...(avatarUrl ? { avatarUrl } : {}),
},
});
+8 -8
View File
@@ -11,8 +11,8 @@ export type ExternalEntryTarget =
export interface ExternalEntryPayload {
deviceId?: string | null;
facebookId?: string | null;
facebookAppId?: string | null;
asid?: string | null;
psid?: string | null;
avatarUrl?: string | null;
}
@@ -37,8 +37,8 @@ export function resolveExternalEntryTarget({
export async function persistExternalEntryPayload({
deviceId,
facebookId,
facebookAppId,
asid,
psid,
avatarUrl,
}: ExternalEntryPayload): Promise<void> {
const authStorage = AuthStorage.getInstance();
@@ -48,11 +48,11 @@ export async function persistExternalEntryPayload({
if (hasValue(deviceId)) {
tasks.push(authStorage.setDeviceId(deviceId));
}
if (hasValue(facebookId)) {
tasks.push(authStorage.setFacebookId(facebookId));
if (hasValue(asid)) {
tasks.push(authStorage.setAsid(asid));
}
if (hasValue(facebookAppId)) {
tasks.push(authStorage.setFacebookAppId(facebookAppId));
if (hasValue(psid)) {
tasks.push(authStorage.setPsid(psid));
}
if (hasValue(avatarUrl)) {
tasks.push(userStorage.setAvatarUrl(avatarUrl));
+7 -7
View File
@@ -148,7 +148,7 @@ export const syncFacebookBackendActor = fromPromise<
/**
* 用 accessToken 调 Facebook Graph `/me` 拉用户资料,写本地:
* - `id` → `AuthStorage.setFacebookId()`(独立于后端 userId
* - `id` → `AuthStorage.setAsid()`(独立于后端 userId
* - `pictureUrl` → `UserStorage.setAvatarUrl()`(头像专用 slot,便于快速读取)
*
* 全部 best-effort:单条失败只 warn,不抛错。`pictureUrl == null` 也算成功
@@ -174,11 +174,11 @@ async function persistFacebookProfile(accessToken: string): Promise<void> {
const authStorage = AuthStorage.getInstance();
if (fbUser.id) {
const r = await authStorage.setFacebookId(fbUser.id);
const r = await authStorage.setAsid(fbUser.id);
if (!r.success) {
log.warn(
{ err: r.error.message },
"[auth-machine] syncFacebookBackendActor: setFacebookId failed",
"[auth-machine] syncFacebookBackendActor: setAsid failed",
);
}
}
@@ -247,13 +247,13 @@ export const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
return "email" as LoginStatus;
}
// 3. 外部浏览器深链同步:仅 fbid 落地时,用它换业务 token。
const facebookIdR = await storage.getFacebookId();
if (facebookIdR.success && facebookIdR.data) {
// 3. 外部浏览器深链同步:仅 asid 落地时,用它换业务 token。
const asidR = await storage.getAsid();
if (asidR.success && asidR.data) {
const authRepo = getAuthRepository();
const avatarUrlR = await UserStorage.getInstance().getAvatarUrl();
const result = await authRepo.facebookIdLogin({
fbId: facebookIdR.data,
fbId: asidR.data,
avatarUrl: avatarUrlR.success ? avatarUrlR.data ?? undefined : undefined,
});