feat: add auth mode and refactor frontend (#21)

This commit is contained in:
Dream Hunter
2023-09-09 16:00:10 +08:00
committed by GitHub
parent 9d18f7b59b
commit a395b951e8
10 changed files with 311 additions and 239 deletions

View File

@@ -106,6 +106,8 @@ node_compat = true
[vars]
PREFIX = "tmp" # 要处理的邮箱名称前缀
# 如果你想要你的网站私有,取消下面的注释,并修改密码
# PASSWORDS = ["123", "456"]
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔

View File

@@ -41,6 +41,8 @@ pnpm install
# copy wrangler.toml.template to wrangler.toml
# and add your d1 config and these config
# PREFIX = "tmp" - the email create will be like tmp<xxxxx>@DOMAIN
# IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES
# PASSWORDS = ["123", "456"]
# DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] you domain name
# JWT_SECRET = "xxx"
# BLACK_LIST = ""

View File

@@ -1,124 +1,59 @@
<script setup>
import { NMessageProvider, NGrid, NBackTop, NLayoutHeader, NInput } from 'naive-ui'
import { NGi, NSpace, NButton, NConfigProvider, NSelect, NModal } from 'naive-ui'
import { darkTheme, NSwitch, NGlobalStyle, NPopconfirm } from 'naive-ui'
import { NMessageProvider, NGrid, NBackTop, NSpin } from 'naive-ui'
import { NGi, NSpace, NButton, NConfigProvider } from 'naive-ui'
import { darkTheme, NGlobalStyle } from 'naive-ui'
import { zhCN } from 'naive-ui'
import { computed, ref } from 'vue'
import { useStorage } from '@vueuse/core'
import { computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import Content from './Content.vue'
import Content from './views/Content.vue'
import Header from './views/Header.vue'
import { useGlobalState } from './store'
const jwt = useStorage('jwt')
const localeCache = useStorage('locale', 'zhCN')
const themeSwitch = useStorage('themeSwitch', false)
const { localeCache, themeSwitch, loading } = useGlobalState()
const theme = computed(() => themeSwitch.value ? darkTheme : null)
const showLogin = ref(false)
const password = ref('')
const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null)
const login = () => {
jwt.value = password.value;
location.reload()
}
const logout = () => {
jwt.value = '';
location.reload()
}
const changeLocale = (locale) => {
localeCache.value = locale;
location.reload()
}
const { t, locale } = useI18n({
const { locale } = useI18n({
useScope: 'global',
locale: localeCache.value || 'zh',
messages: {
en: {
title: 'Cloudflare Temp Email',
dark: 'Dark',
light: 'Light',
login: 'Login',
logout: 'Logout',
logoutConfirm: 'Are you sure to logout?',
},
zh: {
title: 'Cloudflare 临时邮件',
dark: '暗色',
light: '亮色',
login: '登录',
logout: '登出',
logoutConfirm: '确定要登出吗?',
}
}
});
locale.value = localeCache.value;
onMounted(async () => {
const token = import.meta.env.VITE_CF_WEB_ANALY_TOKEN;
const exist = document.querySelector('script[src="https://static.cloudflareinsights.com/beacon.min.js"]') !== null
if (token && !exist) {
const script = document.createElement('script');
script.defer = true;
script.src = 'https://static.cloudflareinsights.com/beacon.min.js';
script.dataset.cfBeacon = `{ token: ${token} }`;
document.body.appendChild(script);
}
});
</script>
<template>
<n-config-provider :locale="localeConfig" :theme="theme">
<n-global-style />
<n-message-provider>
<n-grid x-gap="12" :cols="8">
<n-gi span="1"></n-gi>
<n-gi span="6">
<div class="main">
<n-space vertical>
<n-layout-header>
<div>
<h2>{{ t('title') }}</h2>
</div>
<div>
<n-button v-if="localeCache == 'zh'" @click="changeLocale('en')">English</n-button>
<n-button v-else @click="changeLocale('zh')">中文</n-button>
<n-switch v-model:value="themeSwitch">
<template #checked>
{{ t('dark') }}
</template>
<template #unchecked>
{{ t('light') }}
</template>
</n-switch>
<n-popconfirm v-if="jwt" @positive-click="logout">
<template #trigger>
<n-button tertiary round type="primary">
{{ t('logout') }}
</n-button>
</template>
<template #default>
<span>
{{ t('logoutConfirm') }}
</span>
</template>
</n-popconfirm>
<n-button v-else tertiary @click="showLogin = true" round type="primary">
{{ t('login') }}
</n-button>
<n-button tag="a" target="_blank" tertiary type="primary" round
href="https://github.com/dreamhunter2333/cloudflare_temp_email">Star on Github
</n-button>
</div>
</n-layout-header>
<Content />
</n-space>
</div>
</n-gi>
<n-gi span="1"></n-gi>
</n-grid>
<n-back-top :right="100" />
<n-modal v-model:show="showLogin" preset="dialog" title="Dialog">
<template #header>
<div>{{ t('login') }}</div>
</template>
<n-input v-model:value="password" type="textarea" :autosize="{
minRows: 3
}" />
<template #action>
<n-button @click="login" size="small" tertiary round type="primary">
{{ t('login') }}
</n-button>
</template>
</n-modal>
</n-message-provider>
<n-spin description="loading..." :show="loading">
<n-message-provider>
<n-grid x-gap="12" :cols="8">
<n-gi span="1"></n-gi>
<n-gi span="6">
<div class="main">
<n-space vertical>
<Header />
<Content />
</n-space>
</div>
</n-gi>
<n-gi span="1"></n-gi>
</n-grid>
<n-back-top :right="100" />
</n-message-provider>
</n-spin>
</n-config-provider>
</template>
@@ -156,10 +91,4 @@ locale.value = localeCache.value;
.n-space {
height: 100%;
}
.n-layout-header {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>

66
frontend/src/api/index.js Normal file
View File

@@ -0,0 +1,66 @@
import { useGlobalState } from '../store'
const API_BASE = import.meta.env.VITE_API_BASE || "";
const { loading, auth, jwt, openSettings, showAuth } = useGlobalState();
const apiFetch = async (path, options = {}) => {
loading.value = true;
try {
const response = await fetch(`${API_BASE}${path}`, {
method: options.method || 'GET',
headers: {
'x-custom-auth': auth.value,
'Authorization': `Bearer ${jwt.value}`,
'Content-Type': 'application/json',
},
});
if (response.status === 401 && openSettings.value.auth) {
showAuth.value = true;
throw new Error("Unauthorized");
}
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}` || "error");
}
const data = await response.json();
return data;
} finally {
loading.value = false;
}
}
const getOpenSettings = async (message) => {
try {
const res = await api.fetch("/open_api/settings");
openSettings.value = {
prefix: res["prefix"] || "",
auth: res["auth"] || false,
domains: res["domains"].map((domain) => {
return {
label: domain,
value: domain
}
})
};
if (openSettings.value.auth && !auth.value) {
showAuth.value = true;
}
} catch (error) {
message.error(error.message || "error");
}
}
const getSettings = async () => {
if (typeof jwt.value != 'string' || jwt.value.trim() === '' || jwt.value === 'undefined') {
return "";
}
loading.value = true;
const res = await apiFetch("/api/settings");;
return res["address"];
}
export const api = {
fetch: apiFetch,
getSettings: getSettings,
getOpenSettings: getOpenSettings,
}

View File

@@ -0,0 +1,30 @@
import { ref } from "vue";
import { createGlobalState, useStorage } from '@vueuse/core'
export const useGlobalState = createGlobalState(
() => {
const loading = ref(false);
const openSettings = ref({
prefix: '',
auth: false,
domains: [{
label: 'test.com',
value: 'test.com'
}]
})
const showAuth = ref(false);
const auth = useStorage('auth', '');
const jwt = useStorage('jwt', '');
const localeCache = useStorage('locale', 'zhCN');
const themeSwitch = useStorage('themeSwitch', false);
return {
loading,
openSettings,
showAuth,
auth,
jwt,
localeCache,
themeSwitch
}
},
)

View File

@@ -1,19 +1,19 @@
<script setup>
import { NSpace, NAlert, NSwitch, NCard, NInput, NInputGroupLabel } from 'naive-ui'
import { NSpin, NButton, NLayout, NInputGroup, NModal, NSelect } from 'naive-ui'
import { NList, NListItem, NThing, NTag, NNumberAnimation } from 'naive-ui'
import { NButton, NLayout, NInputGroup, NModal, NSelect } from 'naive-ui'
import { NList, NListItem, NThing, NTag } from 'naive-ui'
import { watch, onMounted, ref } from "vue";
import { useStorage } from '@vueuse/core'
import useClipboard from 'vue-clipboard3'
import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../store'
import { api } from '../api'
const { toClipboard } = useClipboard()
const message = useMessage()
const jwt = useStorage('jwt')
const address = ref("")
const loading = ref(false)
const { jwt, loading, openSettings } = useGlobalState()
const autoRefresh = ref(false)
const data = ref([])
const API_BASE = import.meta.env.VITE_API_BASE || "";
@@ -22,16 +22,8 @@ const showPassword = ref(false)
const showNewEmail = ref(false)
const emailName = ref("")
const emailDomain = ref("")
const openSettings = ref({
prefix: 'test',
domains: [{
label: 'test.com',
value: 'test.com'
}]
})
const { t, locale } = useI18n({
useScope: 'global',
const { t } = useI18n({
locale: 'zh',
messages: {
en: {
@@ -86,27 +78,11 @@ const refresh = async () => {
if (typeof address.value != 'string' || address.value.trim() === '') {
return;
}
loading.value = true;
try {
const response = await fetch(`${API_BASE}/api/mails`, {
method: "GET",
headers: {
"Authorization": `Bearer ${jwt.value}`,
"Content-Type": "application/json"
},
});
if (!response.ok) {
message.error(`${response.status} ${await response.text()}` || "error");
throw new Error(`${response.status} ${await response.text()}` || "error");
}
let res = await response.json();
data.value = res;
data.value = await api.fetch("/api/mails");
} catch (error) {
message.error(error.message || "error");
console.error(error);
} finally {
loading.value = false;
}
};
@@ -121,117 +97,31 @@ const copy = async () => {
const newEmail = async () => {
try {
loading.value = true;
let url = `${API_BASE}/api/new_address`;
url = `${url}?name=${emailName.value || ''}`;
url = `${url}&domain=${emailDomain.value || ''}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json"
},
});
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}` || "error");
}
let res = await response.json();
const res = await api.fetch(
`/api/new_address`
+ `?name=${emailName.value || ''}`
+ `&domain=${emailDomain.value || ''}`
);
jwt.value = res["jwt"];
address.value = await api.getSettings();
await refresh();
showNewEmail.value = false;
showPassword.value = true;
} catch (error) {
message.error(error.message || "error");
console.error(error);
} finally {
loading.value = false;
}
};
const getOpenSettings = async () => {
loading.value = true;
try {
const response = await fetch(`${API_BASE}/open_api/settings`, {
method: "GET",
headers: {
"Content-Type": "application/json"
},
});
if (!response.ok) {
message.error(`${response.status} ${await response.text()}` || "error");
return;
}
let res = await response.json();
openSettings.value = {
prefix: res["prefix"] || "",
domains: res["domains"].map((domain) => {
return {
label: domain,
value: domain
}
})
};
emailDomain.value = openSettings.value.domains[0].value;
} catch (error) {
message.error(error.message || "error");
console.error(error);
}
finally {
loading.value = false;
}
}
const getSettings = async (jwt) => {
if (typeof jwt != 'string' || jwt.trim() === '' || jwt === 'undefined') {
return;
}
loading.value = true;
try {
const response = await fetch(`${API_BASE}/api/settings`, {
method: "GET",
headers: {
"Authorization": `Bearer ${jwt}`,
"Content-Type": "application/json"
},
});
if (!response.ok) {
message.error(`${response.status} ${await response.text()}` || "error");
console.error(response);
address.value = "";
return;
}
let res = await response.json();
address.value = res["address"];
await refresh();
} finally {
loading.value = false;
}
}
watch(jwt, async (jwt, old) => getSettings(jwt))
onMounted(async () => {
getOpenSettings()
getSettings(jwt.value)
await api.getOpenSettings(message);
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
address.value = await api.getSettings();
await refresh();
const token = import.meta.env.VITE_CF_WEB_ANALY_TOKEN;
const exist = document.querySelector('script[src="https://static.cloudflareinsights.com/beacon.min.js"]') !== null
if (token && !exist) {
const script = document.createElement('script');
script.defer = true;
script.src = 'https://static.cloudflareinsights.com/beacon.min.js';
script.dataset.cfBeacon = `{ token: ${token} }`;
document.body.appendChild(script);
}
});
</script>
<template>
<n-spin description="loading..." :show="loading">
<div>
<n-layout>
<n-alert :type='address ? "info" : "warning"' show-icon>
<span v-if="address">
@@ -316,7 +206,7 @@ onMounted(async () => {
<template #action>
</template>
</n-modal>
</n-spin>
</div>
</template>
<style scoped>

View File

@@ -0,0 +1,141 @@
<script setup>
import { NGrid, NLayoutHeader, NInput } from 'naive-ui'
import { NButton, NSelect, NModal } from 'naive-ui'
import { NSwitch, NPopconfirm } from 'naive-ui'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../store'
import { api } from '../api'
const { jwt, localeCache, themeSwitch, showAuth, auth } = useGlobalState()
const showLogin = ref(false)
const password = ref('')
const login = async () => {
try {
await api.getSettings()
jwt.value = password.value;
location.reload()
} catch (error) {
message.error(error.message || "error");
}
}
const logout = () => {
jwt.value = '';
location.reload()
}
const authFunc = async () => {
try {
location.reload()
} catch (error) {
message.error(error.message || "error");
}
}
const changeLocale = (locale) => {
localeCache.value = locale;
location.reload()
}
const { t } = useI18n({
locale: localeCache.value || 'zh',
messages: {
en: {
title: 'Cloudflare Temp Email',
dark: 'Dark',
light: 'Light',
login: 'Login',
logout: 'Logout',
logoutConfirm: 'Are you sure to logout?',
auth: 'Auth',
authTip: 'Please enter the correct auth code',
},
zh: {
title: 'Cloudflare 临时邮件',
dark: '暗色',
light: '亮色',
login: '登录',
logout: '登出',
logoutConfirm: '确定要登出吗?',
auth: '授权',
authTip: '请输入正确的授权码',
}
}
});
</script>
<template>
<n-layout-header>
<div>
<h2>{{ t('title') }}</h2>
</div>
<div>
<n-button v-if="localeCache == 'zh'" @click="changeLocale('en')">English</n-button>
<n-button v-else @click="changeLocale('zh')">中文</n-button>
<n-switch v-model:value="themeSwitch">
<template #checked>
{{ t('dark') }}
</template>
<template #unchecked>
{{ t('light') }}
</template>
</n-switch>
<n-popconfirm v-if="jwt" @positive-click="logout">
<template #trigger>
<n-button tertiary round type="primary">
{{ t('logout') }}
</n-button>
</template>
<template #default>
<span>
{{ t('logoutConfirm') }}
</span>
</template>
</n-popconfirm>
<n-button v-else tertiary @click="showLogin = true" round type="primary">
{{ t('login') }}
</n-button>
<n-button tag="a" target="_blank" tertiary type="primary" round
href="https://github.com/dreamhunter2333/cloudflare_temp_email">Star on Github
</n-button>
</div>
<n-modal v-model:show="showLogin" preset="dialog" title="Dialog">
<template #header>
<div>{{ t('login') }}</div>
</template>
<n-input v-model:value="password" type="textarea" :autosize="{
minRows: 3
}" />
<template #action>
<n-button @click="login" size="small" tertiary round type="primary">
{{ t('login') }}
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showAuth" preset="dialog" title="Dialog">
<template #header>
<div>{{ t('auth') }}</div>
</template>
<p>{{ t('authTip') }}</p>
<n-input v-model:value="auth" type="textarea" :autosize="{
minRows: 3
}" />
<template #action>
<n-button @click="authFunc" size="small" tertiary round type="primary">
{{ t('auth') }}
</n-button>
</template>
</n-modal>
</n-layout-header>
</template>
<style scoped>
.n-layout-header {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>

View File

@@ -22,6 +22,9 @@ api.get('/open_api/settings', async (c) => {
return c.json({
"prefix": c.env.PREFIX,
"domains": c.env.DOMAINS,
"auth": (
c.env.PASSWORDS && c.env.PASSWORDS.length > 0
) ? true : false,
});
})

View File

@@ -8,6 +8,13 @@ import { email } from './email';
const app = new Hono()
app.use('/*', cors());
app.use('/api/*', async (c, next) => {
// check header x-custom-auth
if (c.env.PASSWORDS && c.env.PASSWORDS.length > 0) {
const auth = c.req.headers.get("x-custom-auth");
if (!auth || !c.env.PASSWORDS.includes(auth)) {
return c.text("Need Password", 401)
}
}
if (c.req.path.startsWith("/api/new_address")) {
await next();
return;

View File

@@ -5,6 +5,8 @@ node_compat = true
[vars]
PREFIX = "tmp"
# IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES
# PASSWORDS = ["123", "456"]
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
JWT_SECRET = "xxx"
BLACK_LIST = ""