Skip to content

Commit

Permalink
feat: invite user
Browse files Browse the repository at this point in the history
  • Loading branch information
jiyuujin committed Apr 27, 2024
1 parent 1df36b7 commit 7a4488f
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 4 deletions.
2 changes: 2 additions & 0 deletions apps/web-docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ export default defineConfig({
nav: [
{ text: 'Top', link: '/' },
{ text: 'CSS', link: '/css/getting-started' },
{ text: 'Supabase', link: '/supabase/getting-started' },
],

sidebar: [
{
text: 'Examples',
items: [
{ text: 'CSS', link: '/css/getting-started' },
{ text: 'Supabase', link: '/supabase/getting-started' },
],
},
],
Expand Down
3 changes: 3 additions & 0 deletions apps/web-docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ hero:
- theme: brand
text: CSS
link: /css/getting-started
- theme: brand
text: Supabase
link: /supabase/getting-started
---
75 changes: 75 additions & 0 deletions apps/web-docs/supabase/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Supabase

昨年に続いて [Supabase](https://supabase.com/) のお世話になります。

## 環境構築

ダッシュボードより各種変数を発行、手元の環境でそれらを使えるよう準備します。

データベースへの追加、更新する際は URL (SUPABASE_URL) と Anon Key (SUPABASE_KEY) が必要となります。

```.env
SUPABASE_URL=
SUPABASE_KEY=
```

[`useRuntimeConfig`](https://nuxt.com/docs/api/composables/use-runtime-config) を利用して、各種変数へアクセスできることを確認してください。

```ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
supabaseUrl: process.env.SUPABASE_URL,
supabaseKey: process.env.SUPABASE_KEY,
serviceKey: process.env.SERVICE_KEY,
},
},
})
```

なお、ここで supabase.redirect に `false` を設定しないと、強制的にログイン画面へ遷移されるようになっています。

```ts
export default defineNuxtConfig({
supabase: {
redirect: false,
},
})
```

## メールアドレスを使って招待

[`inviteUserByEmail`](https://supabase.com/docs/reference/javascript/auth-admin-inviteuserbyemail) のお世話になります。事前に Supabase の Auth Admin クライアントを作成する必要があり、直接 Web ブラウザからそれを操作することができません。

Service Role Key も発行しつつ、合わせてこちらも `useRuntimeConfig` を利用してアクセスできることを確認してください。

```ts
import { defineEventHandler, useRuntimeConfig } from '#imports'
import { createClient } from '@supabase/supabase-js'

export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const supabaseUrl = config.public.supabaseUrl
const serviceKey = config.public.serviceKey

if (!supabaseUrl || !serviceKey) {
return event.node.res.end('No authentication')
}

const supabase = createClient(supabaseUrl, serviceKey)
const { error } = await supabase.auth.admin.inviteUserByEmail(email)

if (error) {
return event.node.res.end(error)
}
})
```

raw_user_meta_data に `{ user_role: 'admin' }` を付与しつつ、それが追加された時に限って public.admin_users へデータが insert される設計を取りました。

```ts
const { error } = await supabase.auth.admin.inviteUserByEmail(
email,
{ data: { user_role: 'admin' } },
)
```
1 change: 1 addition & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ NUXT_NEWT_FORM_UID=
NUXT_RECAPTCHA_WEBSITE_KEY=
SUPABASE_URL=
SUPABASE_KEY=
SERVICE_KEY=
AVAILABLE_APPLY_SPONSOR=
23 changes: 23 additions & 0 deletions apps/web/app/composables/useFormError.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { ref } from 'vue'

export function useFormError() {
const idError = ref('')
const nameError = ref('')
const emailError = ref('')
const detailError = ref('')

function validateId(value: string) {
if (value === '') {
idError.value = 'IDを入力してください'
return
}
nameError.value = ''
}

function validateName(value: string) {
if (value === '') {
nameError.value = '名前を入力してください'
Expand All @@ -24,6 +33,17 @@ export function useFormError() {
emailError.value = ''
}

function validateAdminEmail(value: string) {
if (!/[A-Za-z0-9._%+-]+\+supaadmin@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}/.test(value)) {
emailError.value = 'メールアドレスの形式を確認してください'
return
}
console.log('email: ', value)

validateEmail(value)
console.log('emailError: ', emailError)
}

function validateDetail(value: string) {
if (value === '') {
detailError.value = '問い合わせ内容を入力してください'
Expand All @@ -33,11 +53,14 @@ export function useFormError() {
}

return {
idError,
nameError,
emailError,
detailError,
validateId,
validateName,
validateEmail,
validateAdminEmail,
validateDetail,
}
}
32 changes: 32 additions & 0 deletions apps/web/app/composables/useInvitation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { computed, ref } from 'vue'
import { useFormError } from './useFormError'

export function useInvitation() {
const email = ref('')
const id = ref('')
const { ...rest } = useFormError()

const isSubmittingId = computed(() => {
if (!id.value) return false
return rest.idError.value === ''
})

const isSubmittingEmail = computed(() => {
if (!email.value) return false
return rest.emailError.value === ''
})

async function publish(type: 'invite' | 'delete', target: string) {
await $fetch(`/api/${type}-user`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
},
body: JSON.stringify(type === 'invite' ? { email: target } : { id: target }),
})
}

return { publish, isSubmittingId, isSubmittingEmail, email, id, ...rest }
}
87 changes: 87 additions & 0 deletions apps/web/app/pages/staff/invite.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script setup lang="ts">
import { useInvitation } from '~/composables/useInvitation'
import { useFormError } from '~/composables/useFormError'
const { publish, isSubmittingEmail, isSubmittingId, email, id } = useInvitation()
const { emailError, idError, validateAdminEmail, validateId } = useFormError()
function updateEmail(e: any) {
email.value = e.target.value
}
function updateId(e: any) {
id.value = e.target.value
}
function inviteUser() {
publish('invite', email.value)
}
function deleteUser() {
publish('delete', id.value)
}
</script>

<template>
<main>
<div class="invite-root">
<h1 class="title">
Invite or Delete User
</h1>
<VFInputField
id="email"
v-model="email"
name="email"
:label="$t('form.form_email_label')"
placeholder="[email protected]"
required
:error="emailError"
@input="updateEmail"
@blur="validateAdminEmail"
/>
<VFSubmitButton :disabled="!isSubmittingEmail" @click="inviteUser">
Invite User
</VFSubmitButton>
<VFInputField
id="id"
v-model="id"
name="id"
label="ID"
placeholder="User ID"
required
:error="idError"
@input="updateId"
@blur="validateId"
/>
<VFSubmitButton :disabled="!isSubmittingId" @click="deleteUser">
Delete User
</VFSubmitButton>
</div>
</main>
</template>

<style scoped>
@import url('../../assets/base.css');
@import url('../../assets/media.css');
main {
--header-height: calc(var(--unit) * 10);
padding: calc(var(--header-height) + 120px) 20px 0;
background: color(--color-white);
}
.invite-root {
display: grid;
gap: 40px;
max-width: 768px;
margin: 0 auto;
width: 100%;
grid-template-columns: minmax(0, 1fr);
}
.title {
text-align: center;
font-size: 45px;
color: var(--color-vue-blue);
}
.invite-action button {
height: 28px;
}
</style>
26 changes: 26 additions & 0 deletions apps/web/app/server/api/delete-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineEventHandler, useRuntimeConfig } from '#imports'
import { createClient } from '@supabase/supabase-js'

export default defineEventHandler(async (event) => {
const body = await readBody(event)
const id: string = body.id.toString()

if (!id) {
return event.node.res.end('No id')
}

const config = useRuntimeConfig()
const supabaseUrl = config.public.supabaseUrl
const serviceKey = config.public.serviceKey

if (!supabaseUrl || !serviceKey) {
return event.node.res.end('No authentication')
}

const supabase = createClient(supabaseUrl, serviceKey)
const { error } = await supabase.auth.admin.deleteUser(id)

if (error) {
return event.node.res.end(error)
}
})
29 changes: 29 additions & 0 deletions apps/web/app/server/api/invite-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineEventHandler, useRuntimeConfig } from '#imports'
import { createClient } from '@supabase/supabase-js'

export default defineEventHandler(async (event) => {
const body = await readBody(event)
const email: string = body.email.toString()

if (!email) {
return event.node.res.end('No email')
}

const config = useRuntimeConfig()
const supabaseUrl = config.public.supabaseUrl
const serviceKey = config.public.serviceKey

if (!supabaseUrl || !serviceKey) {
return event.node.res.end('No authentication')
}

const supabase = createClient(supabaseUrl, serviceKey)
const { error } = await supabase.auth.admin.inviteUserByEmail(
email,
{ data: { user_role: 'admin' } },
)

if (error) {
return event.node.res.end(error)
}
})
2 changes: 1 addition & 1 deletion apps/web/app/server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
"extends": "../../.nuxt/tsconfig.server.json"
}
12 changes: 9 additions & 3 deletions apps/web/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export default defineNuxtConfig({
}),
],
},
serverMiddleware: [
'~/api/invite-user.ts',
'~/api/delete-user.ts',
],
hooks: {
async 'nitro:config'(nitroConfig) {
if (nitroConfig.dev) {
Expand All @@ -92,7 +96,8 @@ export default defineNuxtConfig({

const supabaseUrl = process.env.SUPABASE_URL
const supabaseKey = process.env.SUPABASE_KEY
if (!supabaseUrl || !supabaseKey) return
const serviceKey = process.env.SERVICE_KEY
if (!supabaseUrl || !supabaseKey || serviceKey) return
},
},
build: {
Expand All @@ -112,8 +117,9 @@ export default defineNuxtConfig({
newtFormUid: process.env.NUXT_NEWT_FORM_UID,
reCaptchaWebsiteKey: process.env.NUXT_RECAPTCHA_WEBSITE_KEY,
// supabase
supabaseProjectUrl: process.env.SUPABASE_URL,
supabaseApiKey: process.env.SUPABASE_KEY,
supabaseUrl: process.env.SUPABASE_URL,
supabaseKey: process.env.SUPABASE_KEY,
serviceKey: process.env.SERVICE_KEY,
// feature
availableApplySponsor: process.env.AVAILABLE_APPLY_SPONSOR,
},
Expand Down
Loading

0 comments on commit 7a4488f

Please sign in to comment.