Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions apps/api/src/services/auth/tokenService.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const ACTIVITY_UPDATE_INTERVAL_SEC = 60;
const localActivityThrottle = new Map();

// 默认有效期 (秒)
const DEFAULT_SESSION_ACCESS_EXPIRY = 60 * 60 * 24; // 24 小时
const DEFAULT_SESSION_REFRESH_EXPIRY = 60 * 60 * 24 * 30; // 30
const DEFAULT_SESSION_ACCESS_EXPIRY = 60 * 60 * 24 * 7; // 7 天
const DEFAULT_SESSION_REFRESH_EXPIRY = 60 * 60 * 24 * 90; // 90
const DEFAULT_OAUTH_ACCESS_EXPIRY = 60 * 60; // 1 小时
const DEFAULT_OAUTH_REFRESH_EXPIRY = 60 * 60 * 24 * 30; // 30 天
const DEFAULT_EDITOR_ACCESS_EXPIRY = 60 * 60 * 24 * 365; // 1 年
Expand Down Expand Up @@ -617,12 +617,16 @@ export async function refreshAccessToken(rawRefreshToken, ipAddress = null, user

const accessTokenExpiry =
record.type === "oauth" ? DEFAULT_OAUTH_ACCESS_EXPIRY : await getSessionAccessExpiry();
const refreshTokenExpiry =
record.type === "oauth" ? DEFAULT_OAUTH_REFRESH_EXPIRY : await getSessionRefreshExpiry();
const now = Date.now();
const rawAccess = generateRawToken();
const rawRefresh = generateRawToken();
const accessHash = hashToken(rawAccess);
const newRefreshHash = hashToken(rawRefresh);
const accessExpiresAt = new Date(now + accessTokenExpiry * 1000);
// 滚动刷新令牌过期时间:每次刷新都延长,保持用户活跃时不过期
const refreshExpiresAt = new Date(now + refreshTokenExpiry * 1000);

const updateResult = await prisma.ow_tokens.updateMany({
where: {
Expand All @@ -635,6 +639,7 @@ export async function refreshAccessToken(rawRefreshToken, ipAddress = null, user
token_prefix: tokenDisplayPrefix(rawAccess),
refresh_token_hash: newRefreshHash,
expires_at: accessExpiresAt,
refresh_expires_at: refreshExpiresAt,
last_used_at: new Date(now),
last_used_ip: normalizedIp,
user_agent: userAgent?.substring(0, 255) || record.user_agent,
Expand All @@ -656,7 +661,7 @@ export async function refreshAccessToken(rawRefreshToken, ipAddress = null, user
accessToken: rawAccess,
refreshToken: rawRefresh,
expiresAt: accessExpiresAt,
refreshExpiresAt: record.refresh_expires_at,
refreshExpiresAt: refreshExpiresAt,
tokenId: record.id,
scopes: await getEffectiveTokenScopes({
userId: record.user.id,
Expand Down
30 changes: 28 additions & 2 deletions apps/web/src/components/account/SignInFlow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,32 @@
:disabled="loading"
prepend-inner-icon="mdi-account-outline"
/>

<v-expand-transition>
<v-alert
v-if="accountNotFound"
type="info"
variant="tonal"
density="comfortable"
class="mb-4"
>
<div class="text-body-2 mb-2">该用户名尚未注册</div>
<v-btn
size="small"
rounded="lg"
color="primary"
variant="flat"
class="text-none"
:to="registerLink"
@click="onClose"
>
创建新账户
</v-btn>
</v-alert>
</v-expand-transition>

<v-btn
v-if="!accountNotFound"
type="submit"
block
size="large"
Expand All @@ -76,7 +101,7 @@
</v-form>

<v-btn
v-if="passkeySupported"
v-if="passkeySupported && !accountNotFound"
block
size="large"
rounded="lg"
Expand All @@ -90,7 +115,7 @@
使用通行密钥登录
</v-btn>

<OAuthButtons mode="login" divider-text="或使用以下方式登录" />
<OAuthButtons v-if="!accountNotFound" mode="login" divider-text="或使用以下方式登录" />

<div class="d-flex justify-space-between mt-4">
<v-btn
Expand Down Expand Up @@ -473,6 +498,7 @@ const {
countdown,
totpCountdown,
passkeySupported,
accountNotFound,
// 计算
hasPasskey,
chooserMethods,
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/composables/useSignInFlow.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function useSignInFlow(options = {}) {
const availableMethods = ref([]); // 来自 /auth/methods 的原始数组
const challenge = ref(null); // 2FA: { challenge_id, expires_in }
const displayName = ref("");
const accountNotFound = ref(false); // 标识符解析后账户不存在

// —— UI 状态 ——
const loading = ref(false);
Expand Down Expand Up @@ -164,6 +165,7 @@ export function useSignInFlow(options = {}) {
// —— 动作:标识符解析 ——
const submitIdentifier = async () => {
error.value = "";
accountNotFound.value = false;
const id = identifier.value.trim();
if (!id) {
error.value = "请输入邮箱或用户名";
Expand All @@ -172,9 +174,8 @@ export function useSignInFlow(options = {}) {
loading.value = true;
try {
const result = await AuthService.getAuthMethods(id, "login");
// 用户名(非邮箱)明确不存在时直接提示;邮箱不透露存在性(防扫号)
if (result.accountExists === false) {
error.value = "用户不存在,请检查用户名或注册新账户";
accountNotFound.value = true;
return;
}
availableMethods.value = result.availableMethods || [];
Expand Down Expand Up @@ -370,6 +371,7 @@ export function useSignInFlow(options = {}) {
error.value = "";
codeSent.value = false;
magicLinkSent.value = false;
accountNotFound.value = false;
};

// —— 条件式 UI(通行密钥自动填充)——
Expand Down Expand Up @@ -439,6 +441,7 @@ export function useSignInFlow(options = {}) {
countdown,
totpCountdown,
passkeySupported,
accountNotFound,
// 计算
isEmail,
hasPasskey,
Expand Down
Loading
Loading