Files

610 lines
23 KiB
Vue

<script>
/**
* 第三方首次登录绑定:微信扫码和通用 OAuth 使用各自的待绑定会话;先验证授权状态,再以手机/邮箱验证码绑定并校验服务端 JWT。
*/
import { definePageMeta } from "#imports";
import "~/assets/styles/PhoneCodeField.CwyHXfML.css";
import "~/assets/styles/GeetestField.DiieTibK.css";
import "~/assets/styles/CaptchaField.BSN3ZbsD.css";
import "~/assets/styles/bind.CFZQE2UH.css";
definePageMeta({
layout: `blank`,
});
import { useRuntimeConfig } from "#app";
import { toDisplayString } from "vue";
import { onMounted } from "vue";
import { normalizeClass } from "vue";
import { onBeforeUnmount } from "vue";
import { watch } from "vue";
import { createBlock } from "vue";
import { ref } from "vue";
import { withCtx } from "vue";
import { createCommentVNode } from "vue";
import { computed } from "vue";
import { isRef } from "vue";
import { createElementBlock } from "vue";
import { createElementVNode } from "vue";
import { createTextVNode } from "vue";
import { unref } from "vue";
import { defineComponent } from "vue";
import { createVNode } from "vue";
import { openBlock } from "vue";
import { withModifiers } from "vue";
import { useRoute } from "#app";
import { navigateTo } from "#app";
import { useHead } from "#app";
import { NuxtLink } from "#components";
import { useApi } from "~/lib/auth.js";
import { isValidJwt } from "~/lib/auth.js";
import { useAuth } from "~/lib/auth.js";
import { t as useSiteConfig_t } from "~/lib/useSiteConfig.js";
import button from "primevue/button";
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
import message from "primevue/message";
import { n as wechat_binding_n } from "~/lib/wechat-binding.js";
import { t as wechat_binding_t } from "~/lib/wechat-binding.js";
import { r as auth_redirect_r } from "~/lib/auth-redirect.js";
import inputtext from "primevue/inputtext";
import CaptchaField from "~/components/CaptchaField.vue";
import checkbox from "primevue/checkbox";
import { n as oauth_n } from "~/lib/oauth.js";
import PhoneCodeField from "~/components/PhoneCodeField.vue";
import { t as useCaptcha_t } from "~/lib/useCaptcha.js";
import { t as composables_t } from "~/lib/composables-2.js";
import { t as useVerifyCode_t } from "~/lib/useVerifyCode.js";
var ve = {
class: `auth-card bind-card`,
},
ye = {
key: 1,
class: `session-status`,
},
be = [`disabled`],
xe = {
class: `seg`,
},
Se = [`aria-pressed`],
Ce = [`aria-pressed`],
we = {
class: `field`,
},
w = {
for: `bind-account`,
},
T = {
class: `input-row`,
},
E = {
class: `field`,
},
D = {
class: `input-row`,
},
O = {
class: `agreement`,
},
k = {
for: `bind-agreement`,
},
Te = [`href`],
Ee = {
key: 1,
},
De = [`href`],
Oe = {
key: 3,
},
A = _plugin_vue_export_helper_t(
defineComponent({
__name: `bind`,
setup(g) {
(composables_t({
layout: `blank`,
}),
useHead({
title: `绑定账号 - 破碎工坊网络`,
}));
let S = useApi(),
A = useAuth(),
j = useSiteConfig_t(),
M = useRoute(),
N = M.query.provider === `wechat`,
P = N ? wechat_binding_n(sessionStorage) : null,
F = auth_redirect_r(M.query.redirect),
I = ref(`phone`),
L = ref(``),
z = ref(``),
B = ref(!1),
V = ref(!1),
H = ref(``),
U = ref(!1),
W = ref(!1),
loginAttempt = null,
G = !0;
onBeforeUnmount(() => {
G = !1;
});
// 先确认授权仍有效;微信支持已完成登录或 WaitBind,通用 OAuth 解析登录/绑定结果,失效状态不开放绑定。
async function K() {
if (!W.value && G) {
((W.value = !0), (U.value = !1), (H.value = ``));
const attempt = A.prepareLogin();
loginAttempt = attempt;
try {
if (N) {
if (!P) throw Error(`扫码会话已过期,请返回登录重新扫码`);
let e = await S.get(`/mp_weixin_notice/qrcode/status`, {
ticket: P.ticket,
token: P.token,
});
if (!G || !attempt.isCurrent()) return;
if (e.status === `Success` && isValidJwt(e.jwt)) {
(wechat_binding_t(sessionStorage),
attempt.commit(e.jwt),
await navigateTo(F));
return;
}
if (e.status !== `WaitBind`)
throw Error(`扫码会话已失效,请返回登录重新扫码`);
U.value = !0;
return;
}
let t = await S.get(`/oauth/token`);
if (!G || !attempt.isCurrent()) return;
let n = oauth_n(
t,
window.location.origin,
new URL(
String(
useRuntimeConfig().public.apiBase || window.location.origin,
),
window.location.origin,
).origin,
);
if (n.kind === `login`) {
(attempt.commit(n.jwt), await navigateTo(`/`));
return;
}
if (n.kind !== `bind`)
throw Error(`授权会话已过期,请返回登录并重新授权`);
U.value = !0;
} catch (e) {
G &&
attempt.isCurrent() &&
(H.value = e instanceof Error ? e.message : `验证授权会话失败`);
} finally {
G && (W.value = !1);
}
}
}
let q = useCaptcha_t(`oauth`),
{
sending: J,
countdown: Y,
send: ke,
} = useVerifyCode_t({
action: `oauth`,
captcha: q,
error: H,
}),
{
needed: Ae,
provider: je,
captchaId: Me,
complete: Ne,
invalidate: X,
value: Z,
img: Pe,
loading: Fe,
error: Q,
load: Ie,
} = q,
$ = computed(() => {
let e = j.config.value,
t = (e) =>
typeof e == `string` && /^https?:\/\//i.test(e) ? e : ``;
return {
service: t(e?.terms_service_url),
privacy: t(e?.terms_privacy_url),
};
});
watch(I, () => {
((L.value = ``), (z.value = ``), (H.value = ``));
});
// 必须先验证授权会话并同意协议;提交验证码绑定,微信额外携带扫码 ticket/token,成功响应的 JWT 合法后才建立会话。
async function Le() {
if (!V.value && G) {
if (((H.value = ``), !U.value || !loginAttempt.isCurrent())) {
U.value = !1;
H.value = `请先验证授权会话;会话失效时请返回登录重新授权`;
return;
}
if (!L.value.trim() || !z.value.trim()) {
H.value = `请填写账号和验证码`;
return;
}
if (!B.value) {
H.value = `请先阅读并同意服务协议和隐私协议`;
return;
}
V.value = !0;
const attempt = loginAttempt;
try {
let e = q.validationError?.();
if (e) throw Error(e);
let t = await S.post(
N
? `/mp_weixin_notice/qrcode/bind_client`
: `/oauth/client/bind`,
{
type: I.value,
account: L.value.trim(),
phone_code: `86`,
code: z.value.trim(),
...(N && P
? {
ticket: P.ticket,
token: P.token,
captcha: q.value.value,
}
: {}),
},
);
if (!G || !attempt.isCurrent()) return;
if (!isValidJwt(t?.jwt))
throw Error(`绑定会话已失效,请返回登录后重新发起第三方授权`);
(N && wechat_binding_t(sessionStorage),
attempt.commit(t.jwt),
await navigateTo(F));
} catch (e) {
G &&
attempt.isCurrent() &&
((H.value =
e instanceof Error ? e.message : `绑定失败,请重试`),
await q.handleError?.(e));
} finally {
G && (V.value = !1);
}
}
}
return (
onMounted(() => {
(j.load(), K());
}),
// 授权状态未验证时锁定绑定流程;协议勾选、验证码和当前渠道共同决定是否可以提交。
(e, n) => {
let i = message,
a = button,
s = NuxtLink,
u = PhoneCodeField,
g = inputtext,
y = CaptchaField,
b = checkbox;
return (
openBlock(),
createElementBlock(`section`, ve, [
(n[13] ||= createElementVNode(`h1`, null, `绑定账号`, -1)),
(n[14] ||= createElementVNode(
`p`,
{
class: `bind-intro`,
},
`请使用已注册的手机号或邮箱接收验证码,完成与第三方账号的绑定。`,
-1,
)),
unref(H)
? (openBlock(),
createBlock(
i,
{
key: 0,
severity: `error`,
closable: !1,
},
{
default: withCtx(() => [
createTextVNode(toDisplayString(unref(H)), 1),
]),
_: 1,
},
))
: createCommentVNode(``, !0),
unref(U)
? createCommentVNode(``, !0)
: (openBlock(),
createElementBlock(`div`, ye, [
createVNode(
a,
{
label: `验证授权会话`,
loading: unref(W),
onClick: K,
},
null,
8,
[`loading`],
),
createVNode(
s,
{
to: `/login`,
},
{
default: withCtx(() => [
...(n[8] ||= [createTextVNode(`返回登录`, -1)]),
]),
_: 1,
},
),
])),
unref(U)
? (openBlock(),
createElementBlock(
`form`,
{
key: 2,
class: `bind-form`,
onSubmit: withModifiers(Le, [`prevent`]),
},
[
createElementVNode(
`fieldset`,
{
disabled: unref(V),
},
[
(n[9] ||= createElementVNode(
`legend`,
null,
`验证方式`,
-1,
)),
createElementVNode(`div`, xe, [
createElementVNode(
`button`,
{
type: `button`,
class: normalizeClass({
active: unref(I) === `email`,
}),
"aria-pressed": unref(I) === `email`,
onClick: (n[0] ||= (e) =>
(I.value = `email`)),
},
` 邮箱 `,
10,
Se,
),
createElementVNode(
`button`,
{
type: `button`,
class: normalizeClass({
active: unref(I) === `phone`,
}),
"aria-pressed": unref(I) === `phone`,
onClick: (n[1] ||= (e) =>
(I.value = `phone`)),
},
` 手机号 `,
10,
Ce,
),
]),
],
8,
be,
),
createElementVNode(`div`, we, [
createElementVNode(
`label`,
w,
toDisplayString(
unref(I) === `phone` ? `手机号` : `邮箱`,
),
1,
),
createElementVNode(`div`, T, [
unref(I) === `phone`
? (openBlock(), createBlock(u, { key: 0 }))
: createCommentVNode(``, !0),
createVNode(
g,
{
id: `bind-account`,
modelValue: unref(L),
"onUpdate:modelValue": (n[3] ||= (e) =>
isRef(L) ? (L.value = e) : null),
type: unref(I) === `email` ? `email` : `tel`,
placeholder:
unref(I) === `phone`
? `输入手机号`
: `输入邮箱`,
autocomplete: `username`,
disabled: unref(V),
required: ``,
},
null,
8,
[`modelValue`, `type`, `placeholder`, `disabled`],
),
]),
]),
createElementVNode(`div`, E, [
(n[10] ||= createElementVNode(
`label`,
{
for: `bind-code`,
},
`验证码`,
-1,
)),
createElementVNode(`div`, D, [
createVNode(
g,
{
id: `bind-code`,
modelValue: unref(z),
"onUpdate:modelValue": (n[4] ||= (e) =>
isRef(z) ? (z.value = e) : null),
placeholder: `输入验证码`,
autocomplete: `one-time-code`,
disabled: unref(V),
required: ``,
},
null,
8,
[`modelValue`, `disabled`],
),
createVNode(
a,
{
label: unref(Y) ? `${unref(Y)}s` : `获取验证码`,
loading: unref(J),
disabled:
unref(V) || !unref(L).trim() || unref(Y) > 0,
onClick: (n[5] ||= (e) =>
unref(ke)(
unref(I),
unref(L).trim(),
`请输入账号`,
)),
},
null,
8,
[`label`, `loading`, `disabled`],
),
]),
]),
unref(Ae)
? (openBlock(),
createBlock(
y,
{
key: 0,
modelValue: unref(Z),
"onUpdate:modelValue": (n[6] ||= (e) =>
isRef(Z) ? (Z.value = e) : null),
img: unref(Pe),
provider: unref(je),
"captcha-id": unref(Me),
disabled: unref(V) || unref(J),
onVerified: unref(Ne),
onInvalid: unref(X),
loading: unref(Fe),
onRefresh: unref(Ie),
},
null,
8,
[
`modelValue`,
`img`,
`provider`,
`captcha-id`,
`disabled`,
`onVerified`,
`onInvalid`,
`loading`,
`onRefresh`,
],
))
: createCommentVNode(``, !0),
unref(Q)
? (openBlock(),
createBlock(
i,
{
key: 1,
severity: `error`,
closable: !1,
},
{
default: withCtx(() => [
createTextVNode(toDisplayString(unref(Q)), 1),
]),
_: 1,
},
))
: createCommentVNode(``, !0),
createElementVNode(`div`, O, [
createVNode(
b,
{
modelValue: unref(B),
"onUpdate:modelValue": (n[7] ||= (e) =>
isRef(B) ? (B.value = e) : null),
"input-id": `bind-agreement`,
binary: ``,
disabled: unref(V),
},
null,
8,
[`modelValue`, `disabled`],
),
createElementVNode(`label`, k, [
(n[11] ||= createTextVNode(`阅读并同意 `, -1)),
unref($).service
? (openBlock(),
createElementBlock(
`a`,
{
key: 0,
href: unref($).service,
target: `_blank`,
rel: `noopener noreferrer`,
},
`《服务协议》`,
8,
Te,
))
: (openBlock(),
createElementBlock(`span`, Ee, `《服务协议》`)),
(n[12] ||= createTextVNode(` 和 `, -1)),
unref($).privacy
? (openBlock(),
createElementBlock(
`a`,
{
key: 2,
href: unref($).privacy,
target: `_blank`,
rel: `noopener noreferrer`,
},
`《隐私协议》`,
8,
De,
))
: (openBlock(),
createElementBlock(`span`, Oe, `《隐私协议》`)),
]),
]),
createVNode(
a,
{
type: `submit`,
label: `确认绑定`,
loading: unref(V),
disabled:
!unref(B) || !unref(L).trim() || !unref(z).trim(),
fluid: ``,
},
null,
8,
[`loading`, `disabled`],
),
],
32,
))
: createCommentVNode(``, !0),
])
);
}
);
},
}),
[[`__scopeId`, `data-v-146faed1`]],
);
export {};
export default A;
</script>