feat(gateway): verify Kakao account ownership
This commit is contained in:
@@ -5,6 +5,7 @@ import type { inferRouterOutputs } from '@trpc/server';
|
||||
import type { AppRouter } from '@sammo-ts/gateway-api';
|
||||
|
||||
import MapPreview from '../components/MapPreview.vue';
|
||||
import KakaoOtpDialog from '../components/KakaoOtpDialog.vue';
|
||||
import DefaultLayout from '../layouts/DefaultLayout.vue';
|
||||
import { createGameTrpc, type GameRouter } from '../utils/gameTrpc';
|
||||
import { trpc } from '../utils/trpc';
|
||||
@@ -24,6 +25,7 @@ const username = ref('');
|
||||
const password = ref('');
|
||||
const loginError = ref('');
|
||||
const loginLoading = ref(false);
|
||||
const otpChallenge = ref<{ challengeId: string; expiresAt: string; attemptsRemaining: number } | null>(null);
|
||||
const statusLoading = ref(false);
|
||||
const statusError = ref('');
|
||||
const profile = ref<LobbyProfile | null>(null);
|
||||
@@ -92,6 +94,10 @@ const handleLogin = async (): Promise<void> => {
|
||||
username: username.value,
|
||||
credential,
|
||||
});
|
||||
if (result.status === 'otp') {
|
||||
otpChallenge.value = result;
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem('sammo-session-token', result.sessionToken);
|
||||
await router.push('/lobby');
|
||||
} catch (error) {
|
||||
@@ -101,6 +107,12 @@ const handleLogin = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtpVerified = async (sessionToken: string): Promise<void> => {
|
||||
window.localStorage.setItem('sammo-session-token', sessionToken);
|
||||
otpChallenge.value = null;
|
||||
await router.push('/lobby');
|
||||
};
|
||||
|
||||
const handleKakao = async (): Promise<void> => {
|
||||
loginError.value = '';
|
||||
try {
|
||||
@@ -191,6 +203,12 @@ const handlePasswordReset = async (): Promise<void> => {
|
||||
</section>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
<KakaoOtpDialog
|
||||
v-if="otpChallenge"
|
||||
:challenge-id="otpChallenge.challengeId"
|
||||
@verified="handleOtpVerified"
|
||||
@cancel="otpChallenge = null"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import DefaultLayout from '../layouts/DefaultLayout.vue';
|
||||
import KakaoOtpDialog from '../components/KakaoOtpDialog.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
import { sealPassword } from '../utils/passwordEnvelope';
|
||||
|
||||
@@ -21,6 +22,8 @@ const displayName = ref('');
|
||||
const termsAgreed = ref(false);
|
||||
const privacyAgreed = ref(false);
|
||||
const thirdPartyUse = ref(false);
|
||||
const otpChallenge = ref<{ challengeId: string; expiresAt: string; attemptsRemaining: number } | null>(null);
|
||||
const otpSuccessStatus = ref<'login' | 'verified'>('login');
|
||||
const appBase = import.meta.env.BASE_URL;
|
||||
|
||||
const completeExchange = async (): Promise<void> => {
|
||||
@@ -33,6 +36,11 @@ const completeExchange = async (): Promise<void> => {
|
||||
}
|
||||
try {
|
||||
const result = await trpc.auth.kakaoExchange.mutate({ code, state });
|
||||
if (result.status === 'otp') {
|
||||
otpChallenge.value = result;
|
||||
otpSuccessStatus.value = result.successStatus;
|
||||
return;
|
||||
}
|
||||
if (result.status === 'login') {
|
||||
window.localStorage.setItem('sammo-session-token', result.sessionToken);
|
||||
await router.replace('/lobby');
|
||||
@@ -78,6 +86,12 @@ const register = async (): Promise<void> => {
|
||||
privacyAgreed: true,
|
||||
thirdPartyUse: thirdPartyUse.value,
|
||||
});
|
||||
if (result.status === 'otp') {
|
||||
otpChallenge.value = result;
|
||||
otpSuccessStatus.value = result.successStatus;
|
||||
oauthSessionId.value = '';
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem('sammo-session-token', result.sessionToken);
|
||||
await router.replace('/lobby');
|
||||
} catch (error) {
|
||||
@@ -87,6 +101,12 @@ const register = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtpVerified = async (sessionToken: string): Promise<void> => {
|
||||
window.localStorage.setItem('sammo-session-token', sessionToken);
|
||||
otpChallenge.value = null;
|
||||
await router.replace(otpSuccessStatus.value === 'verified' ? '/lobby?verified=1' : '/lobby');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
void completeExchange();
|
||||
});
|
||||
@@ -120,7 +140,13 @@ onMounted(() => {
|
||||
<div class="form-row">
|
||||
<label for="oauth-display-name">닉네임</label>
|
||||
<div>
|
||||
<input id="oauth-display-name" v-model="displayName" minlength="2" maxlength="40" required />
|
||||
<input
|
||||
id="oauth-display-name"
|
||||
v-model="displayName"
|
||||
minlength="2"
|
||||
maxlength="40"
|
||||
required
|
||||
/>
|
||||
<small>깃수가 종료될 때 공개됩니다. 계속 사용할 이름이므로 신중하게 정해주세요.</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,6 +180,12 @@ onMounted(() => {
|
||||
</section>
|
||||
</main>
|
||||
</DefaultLayout>
|
||||
<KakaoOtpDialog
|
||||
v-if="otpChallenge"
|
||||
:challenge-id="otpChallenge.challengeId"
|
||||
@verified="handleOtpVerified"
|
||||
@cancel="otpChallenge = null"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user