Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

新規登録関連画面の実装 #73

Merged
merged 11 commits into from
Dec 16, 2024
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_BASE_URL = http://localhost:4010
8 changes: 8 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dependencies": {
"@types/eslint__js": "^8.42.3",
"eslint-config-prettier": "^9.1.0",
"jwt-decode": "^4.0.0",
"validator": "^13.12.0",
"vue": "^3.5.6",
"vue-router": "^4.3.3"
Expand All @@ -25,6 +26,7 @@
"@eslint/js": "^9.11.0",
"@rushstack/eslint-patch": "^1.8.0",
"@tsconfig/node20": "^20.1.4",
"@types/jwt-decode": "^2.2.1",
"@types/node": "^20.14.5",
"@types/validator": "^13.12.2",
"@vitejs/plugin-vue": "^5.1.3",
Expand Down
11 changes: 10 additions & 1 deletion src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,16 @@
line-height: 1rem;
}

.fontstyle-ui-caption {
.fontstyle-ui-caption-link {
@apply font-primary;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1rem;
text-decoration: underline;
}

.fontstyle-ui-caption-strong {
@apply font-primary;
font-size: 0.75rem;
font-style: normal;
Expand Down
74 changes: 74 additions & 0 deletions src/components/Controls/OAuthButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'

const {
disabled = false,
app,
mode
} = defineProps<{
disabled?: boolean
app: string
mode: 'signup' | 'login'
}>()

const router = useRouter()

async function onOAuthClick() {
try {
if (mode === 'signup') {
if (app === 'Github') {
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/github-oauth2/params`)
if (response.status === 200) {
const responseJson = await response.json()
alert(responseJson.url)
router.push(responseJson.url)
} else if (response.status === 500) {
const responseJson = await response.json()
alert('Internal Server Error: ' + responseJson.message)
} else {
alert(response.status)
}
}
if (app === 'Google') {
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/google-oauth2/params`)
if (response.status === 200) {
const responseJson = await response.json()
router.push(responseJson.url)
} else if (response.status === 500) {
const responseJson = await response.json()
alert('Internal Server Error: ' + responseJson.message)
} else {
alert(response.status)
}
}
if (app === 'traQ') {
router.push('/_oauth/login?redirect=/') // TODO: Redirect to the correct URL
}
}
} catch (error) {
console.error('OAuth Error:', error)
alert('OAuth Error:' + error)
}
}
</script>

<template>
<button
:disabled="disabled"
class="fontstyle-ui-control-strong inline-block space-x-2.5 rounded-lg border border-border-secondary px-3 py-2 text-text-primary enabled:hover:bg-background-secondary disabled:opacity-50"
@click="onOAuthClick"
>
<span v-if="app === 'Github'" class="inline-block align-middle"
><img src="" class="size-5"
/></span>
<span v-if="app === 'Google'" class="inline-block align-middle"
><img src="" class="size-5"
/></span>
<span v-if="app === 'traQ'" class="inline-block align-middle"
><img src="" class="size-5"
/></span>
<span class="inline-block align-middle">{{ app }} で新規登録</span>
</button>
</template>

<style scoped></style>
11 changes: 11 additions & 0 deletions src/utils/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const usernameValidator = (username: string): boolean => {
return /^[A-Za-z0-9_]{5,10}$/.test(username)
}

export const passwordValidator = (password: string): boolean => {
const hasLetter = /[A-Za-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecialChar = /[!@#$%^&*()_+\-={}[\]:;"'<>,.?/]/.test(password);
const isValid = /^[A-Za-z0-9!@#$%^&*()_+\-={}[\]:;"'<>,.?/]{10,64}$/.test(password);
return hasLetter && hasNumber && hasSpecialChar && isValid;
}
35 changes: 32 additions & 3 deletions src/views/SignupAfterMailView.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import BorderedButton from '@/components/Controls/BorderedButton.vue'
import PrimaryButton from '@/components/Controls/PrimaryButton.vue'
const router = useRouter()
function onClose() {
router.push('/')
}
function onResendEmail() {
router.push('/signup')
}
</script>

<template>
<div>
<h1>Signup After Mail</h1>
<div
class="flex items-center justify-center bg-background-tertiary px-8 py-6"
style="height: calc(100vh - 56px)"
>
<div class="max-w-xl space-y-5 rounded-2xl bg-white px-14 py-10">
<div class="fontstyle-ui-title text-left">確認メールを送信しました</div>
<div class="fontstyle-ui-body text-left text-text-secondary">
60分以内に、メールに記載されたリンクから登録フォームにアクセスしてください。
</div>
<div class="flex gap-3">
<div class="flex-1">
<PrimaryButton text="この画面を閉じる" class="w-full" @click="onClose" />
</div>
<div class="flex-1">
<BorderedButton text="メールを再送信する" class="w-full" @click="onResendEmail" />
</div>
</div>
</div>
</div>
</template>

Expand Down
164 changes: 161 additions & 3 deletions src/views/SignupRegisterView.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,166 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { jwtDecode } from 'jwt-decode'
import { usernameValidator, passwordValidator } from '@/utils/validator'
import PasswordTextbox from '@/components/Controls/Textbox/PasswordTextbox.vue'
import PlainTextbox from '@/components/Controls/Textbox/PlainTextbox.vue'
import PrimaryButton from '@/components/Controls/PrimaryButton.vue'

const username = ref('')
const usernameErrorMessage = ref('')
const emailAddress = ref('')
const password = ref('')
const passwordErrorMessage = ref('')
const confirmPassword = ref('')
const confirmPasswordErrorMessage = ref('')

onMounted(() => {
try {
const token = new URLSearchParams(window.location.search).get('token')
if (token) {
const decodedToken = jwtDecode<{ email: string }>(token)
emailAddress.value = decodedToken.email
}
} catch (error) {
console.error('Signup Register Error:', error)
alert('Signup Register Error:' + error)
}
})

const router = useRouter()

async function onSignupRegister() {
try {
let error = false
// check user name
if (!username.value) {
usernameErrorMessage.value = 'ユーザー名を入力してください'
error = true
} else if (!usernameValidator(username.value)) {
usernameErrorMessage.value = '無効なユーザー名'
error = true
} else {
usernameErrorMessage.value = ''
}
// check password
if (!password.value) {
passwordErrorMessage.value = 'パスワードを入力してください'
error = true
} else if (!passwordValidator(password.value)) {
passwordErrorMessage.value = '無効なパスワード'
error = true
} else {
passwordErrorMessage.value = ''
}
// check confirm password
if (!confirmPassword.value) {
confirmPasswordErrorMessage.value = 'パスワード(確認)を入力してください'
error = true
} else if (password.value !== confirmPassword.value) {
confirmPasswordErrorMessage.value = 'パスワードが一致しません'
error = true
} else {
confirmPasswordErrorMessage.value = ''
}
if (error) {
return
}
const token = new URLSearchParams(window.location.search).get('token')
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ userName: username.value, password: password.value, token: token })
})
if (response.status === 201) {
router.push('/login')
} else if (response.status === 400) {
alert('不正なリクエストです')
} else if (response.status === 401) {
alert('Unauthorized')
} else {
alert(response.status)
}
} catch (error) {
console.error('Signup Register Error:', error)
alert('Signup Register Error:' + error)
}
}
</script>

<template>
<div>
<h1>Signup Register</h1>
<div
class="flex items-center justify-center bg-background-tertiary px-8 py-6"
style="height: calc(100vh - 56px)"
>
<div class="max-w-3xl space-y-5 rounded-2xl bg-white px-14 py-10">
<div class="fontstyle-ui-title text-left">新規登録</div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

以下の各種ラベルとフォームもgridで整列すると良さそうです。

<div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このようなformのタイトル?を表すようなものは全てlabel要素で書き換え,それぞれのformとforidにより対応づけをしてください。(このドキュメントを参照してください。 https://developer.mozilla.org/ja/docs/Web/HTML/Element/label)
上記の修正をしても現状では正しくformがフォーカスされないと思いますが,それはTextboxコンポーネントの実装の問題なので別issueで作業しますl。

<span class="fontstyle-ui-body text-status-error">*</span>
<span class="fontstyle-ui-body text-text-primary">がついた項目は必須です。</span>
</div>
<div class="flex flex-col space-y-5 p-2.5">
<div class="flex gap-6">
<label for="username" class="w-50 text-right">
<span class="fontstyle-ui-body-strong text-text-primary">ユーザー名</span>
<span class="fontstyle-ui-body-strong text-status-error">*</span>
</label>
<div class="flex-1">
<PlainTextbox id="username" v-model="username" :error-message="usernameErrorMessage" />
<div class="fontstyle-ui-caption-strong text-nowrap pt-1 text-text-secondary">
文字数は5以上10以下で、半角英数字とアンダースコアのみが使用できます。
</div>
</div>
</div>
<div class="flex gap-6">
<div class="w-50 text-right">
<span class="fontstyle-ui-body-strong text-text-primary">メールアドレス</span>
</div>
<div class="flex-1">
<span class="fontstyle-ui-body w-full px-1 text-text-primary">
{{ emailAddress }}
</span>
</div>
</div>
<div class="flex gap-6">
<label for="password" class="w-50 text-right">
<span class="fontstyle-ui-body-strong text-text-primary">パスワード</span>
<span class="fontstyle-ui-body-strong text-status-error">*</span>
</label>
<div class="flex-1">
<PasswordTextbox
id="password"
v-model="password"
:error-message="passwordErrorMessage"
/>
<div class="fontstyle-ui-caption-strong pt-1 text-text-secondary">
文字数は10以上64以下で、半角英数字と記号が使用できます。
</div>
<div class="fontstyle-ui-caption-strong pt-1 text-text-secondary">
英字、数字、記号がそれぞれ1文字以上含まれている必要があります。
</div>
</div>
</div>
<div class="flex gap-6">
<label for="confirPassword" class="w-50 text-right">
<span class="fontstyle-ui-body-strong text-text-primary">パスワード(確認)</span>
<span class="fontstyle-ui-body-strong text-status-error">*</span>
</label>
<div class="flex-1">
<PasswordTextbox
id="confirmPassword"
v-model="confirmPassword"
:error-message="confirmPasswordErrorMessage"
/>
</div>
</div>
<div class="flex justify-center">
<PrimaryButton text="次へ" @click="onSignupRegister" />
</div>
</div>
</div>
</div>
</template>

Expand Down
Loading
Loading