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

feat: support transfer address from user to user #484

Merged
merged 2 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

- feat: |UI| 随机生成地址时不超过最大长度
- feat: |UI| 邮件时间显示浏览器时区,可在设置中切换显示为 UTC 时间
- feat: 支持转移邮件到其他用户

## v0.7.6

Expand Down
2 changes: 1 addition & 1 deletion db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS raw_mails (
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);

CREATE TABLE IF NOT EXISTS address (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
Expand Down
4 changes: 0 additions & 4 deletions frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import { createApp } from 'vue'
import App from './App.vue'
import { createI18n } from 'vue-i18n'
import router from './router'
import { registerSW } from 'virtual:pwa-register'
import { createHead } from '@unhead/vue'

const disablePwa = import.meta.env.VITE_PWA_DISABLED === 'true';

if (!disablePwa) registerSW({ immediate: true })
const i18n = createI18n({
legacy: false, // you must set `false`, to use Composition API
locale: 'zh', // set locale
Expand Down
67 changes: 65 additions & 2 deletions frontend/src/views/user/AddressManagement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,34 @@ const { locale, t } = useI18n({
mail_count: 'Mail Count',
send_count: 'Send Count',
actions: 'Actions',
changeMailAddress: 'Change Mail Address',
changeMailAddress: 'Change Address',
unbindAddress: 'Unbind Address',
unbindAddressTip: 'Before unbinding, please switch to this email address and save the email address credential.',
transferAddress: 'Transfer Address',
targetUserEmail: 'Target User Email',
transferAddressTip: 'Transfer address to another user will remove the address from your account and transfer it to another user. Are you sure to transfer the address?'
},
zh: {
success: '成功',
name: '名称',
mail_count: '邮件数量',
send_count: '发送数量',
actions: '操作',
changeMailAddress: '切换邮箱地址',
changeMailAddress: '切换地址',
unbindAddress: '解绑地址',
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
transferAddress: '转移地址',
targetUserEmail: '目标用户邮箱',
transferAddressTip: '转移地址到其他用户将会从你的账户中移除此地址并转移给其他用户。确定要转移地址吗?'
}
}
});

const data = ref([])
const showTranferAddress = ref(false)
const currentAddress = ref("")
const currentAddressId = ref(0)
const targetUserEmail = ref('')

const changeMailAddress = async (address_id) => {
try {
Expand Down Expand Up @@ -70,6 +80,35 @@ const unbindAddress = async (address_id) => {
}
}

const transferAddress = async () => {
if (!targetUserEmail.value) {
message.error("targetUserEmail is required");
return;
}
if (!currentAddressId.value) {
message.error("currentAddressId is required");
return;
}
try {
const res = await api.fetch(`/user_api/transfer_address`, {
method: 'POST',
body: JSON.stringify({
address_id: currentAddressId.value,
target_user_email: targetUserEmail.value
})
});
message.success(t('transferAddress') + " " + t('success'));
await fetchData();
showTranferAddress.value = false;
currentAddressId.value = 0;
currentAddress.value = "";
targetUserEmail.value = "";
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}

const fetchData = async () => {
try {
const { results, count: addressCount } = await api.fetch(
Expand Down Expand Up @@ -138,6 +177,18 @@ const columns = [
default: () => `${t('changeMailAddress')}?`
}
),
h(NButton,
{
tertiary: true,
type: "primary",
onClick: () => {
currentAddressId.value = row.id;
currentAddress.value = row.name;
showTranferAddress.value = true;
}
},
{ default: () => t('transferAddress') }
),
h(NPopconfirm,
{
onPositiveClick: () => unbindAddress(row.id)
Expand All @@ -164,6 +215,18 @@ onMounted(async () => {
</script>

<template>
<n-modal v-model:show="showTranferAddress" preset="dialog" :title="t('transferAddress')">
<span>
<p>{{ t("transferAddressTip") }}</p>
<p>{{ t('transferAddress') + ": " + currentAddress }}</p>
<n-input v-model:value="targetUserEmail" :placeholder="t('targetUserEmail')" />
</span>
<template #action>
<n-button :loading="loading" @click="transferAddress" size="small" tertiary type="error">
{{ t('transferAddress') }}
</n-button>
</template>
</n-modal>
<div style="overflow: auto;">
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default defineConfig({
resolvers: [NaiveUiResolver()]
}),
VitePWA({
registerType: process.env.VITE_PWA_DISABLED == "true" ? null : 'autoUpdate',
registerType: null,
devOptions: {
enabled: true
},
Expand Down
91 changes: 90 additions & 1 deletion worker/src/user_api/bind_address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HonoCustomType } from '../types';
import { UserSettings } from "../models";
import { getJsonSetting } from "../utils"
import { CONSTANTS } from "../constants";
import { unbindTelegramByAddress } from '../telegram_api/common';

export default {
bind: async (c: Context<HonoCustomType>) => {
Expand Down Expand Up @@ -89,7 +90,7 @@ export default {
return c.text("Failed to unbind", 500)
}
} catch (e) {
return c.text("Invalid address token", 400)
return c.text("Failed to unbind", 500)
}
return c.json({ success: true })
},
Expand Down Expand Up @@ -139,4 +140,92 @@ export default {
jwt: jwt
})
},
transferAddress: async (c: Context<HonoCustomType>) => {
const { user_id } = c.get("userPayload");
const { address_id, target_user_email } = await c.req.json();
// check if address exists
const address = await c.env.DB.prepare(
`SELECT name FROM address where id = ?`
).bind(address_id).first<string>("name");
if (!address) {
return c.text("Address not found", 400)
}
// check if user exists
const db_user_id = await c.env.DB.prepare(
`SELECT id FROM users where id = ?`
).bind(user_id).first("id");
if (!db_user_id) {
return c.text("User not found", 400)
}
// check if target user exists
const target_user_id = await c.env.DB.prepare(
`SELECT id FROM users where user_email = ?`
).bind(target_user_email).first("id");
if (!target_user_id) {
return c.text("Target user not found", 400)
}
// check target user binded address count
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value);
if (settings.maxAddressCount > 0) {
const { count } = await c.env.DB.prepare(
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
).bind(target_user_id).first<{ count: number }>() || { count: 0 };
if (count >= settings.maxAddressCount) {
return c.text("Target User Max address count reached", 400)
}
}
// check if binded
const db_user_address_id = await c.env.DB.prepare(
`SELECT user_id FROM users_address where user_id = ? and address_id = ?`
).bind(user_id, address_id).first("user_id");
if (!db_user_address_id) return c.text("Address not binded", 400)
// unbind telegram address
await unbindTelegramByAddress(c, address);
// unbind user address
try {
const { success } = await c.env.DB.prepare(
`DELETE FROM users_address where user_id = ? and address_id = ?`
).bind(user_id, address_id).run();
if (!success) {
return c.text("Failed to unbind", 500)
}
} catch (e) {
return c.text("Failed to unbind user", 500)
}
// delete address
await c.env.DB.prepare(
`DELETE FROM address WHERE id = ? `
).bind(address_id).run();
// new address
const { success: newAddressSuccess } = await c.env.DB.prepare(
`INSERT INTO address(name) VALUES(?)`
).bind(address).run();
if (!newAddressSuccess) {
throw new Error("Failed to create address")
}
// find new address id
let new_address_id = await c.env.DB.prepare(
`SELECT id FROM address WHERE name = ?`
).bind(address).first<number | null | undefined>("id");
if (!new_address_id) {
throw new Error("Failed to find new address id")
}
// bind
try {
const { success } = await c.env.DB.prepare(
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
).bind(target_user_id, new_address_id).run();
if (!success) {
return c.text("Failed to bind", 500)
}
} catch (e) {
const error = e as Error;
if (error.message && error.message.includes("UNIQUE")) {
return c.text("Address already binded, please unbind first", 400)
}
return c.text("Failed to bind", 500)
}
return c.json({ success: true })
}
}
1 change: 1 addition & 0 deletions worker/src/user_api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ api.get('/user_api/bind_address', bind_address.getBindedAddresses);
api.post('/user_api/bind_address', bind_address.bind);
api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt);
api.post('/user_api/unbind_address', bind_address.unbind);
api.post('/user_api/transfer_address', bind_address.transferAddress);

// passkey api
api.get('/user_api/passkey', passkey.getPassKeys);
Expand Down