diff --git a/src/api/users.ts b/src/api/users.ts index 3385e3d..4d8ffd5 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -1,9 +1,10 @@ import { BACKEND_URL } from "@/constants" import { PAGE_SIZE, http } from "./common" -interface ProfileField { +export interface ProfileField { name: string; value: string; + verified_at: string | null; } interface Source { @@ -20,6 +21,7 @@ export interface Profile { note: string | null; avatar: string | null; header: string | null; + identity_proofs: ProfileField[], fields: ProfileField[]; followers_count: number; @@ -145,3 +147,28 @@ export async function updateProfile( return profileOrError } } + +export async function getIdentityClaim(authToken: string): Promise { + const url = `${BACKEND_URL}/api/v1/accounts/identity_proof` + const response = await http(url, { authToken }) + const data = await response.json() + return data.claim +} + +export async function createIdentityProof( + authToken: string, + signature: string, +): Promise { + const url = `${BACKEND_URL}/api/v1/accounts/identity_proof` + const response = await http(url, { + method: "POST", + json: { signature: signature.replace(/0x/, "") }, + authToken, + }) + const data = await response.json() + if (response.status !== 200) { + throw new Error(data.message) + } else { + return data + } +} diff --git a/src/assets/forkawesome/check.svg b/src/assets/forkawesome/check.svg new file mode 100644 index 0000000..e419c12 --- /dev/null +++ b/src/assets/forkawesome/check.svg @@ -0,0 +1,2 @@ + + diff --git a/src/utils/ethereum.ts b/src/utils/ethereum.ts index db976a6..9d7fb88 100644 --- a/src/utils/ethereum.ts +++ b/src/utils/ethereum.ts @@ -40,6 +40,15 @@ export async function getWalletAddress(provider: Web3Provider): Promise { + const signature = await signer.signMessage(message) + return signature +} + function generateRandomString(len: number): string { const arr = new Uint8Array(len / 2) window.crypto.getRandomValues(arr) diff --git a/src/views/Profile.vue b/src/views/Profile.vue index d2af3e2..9eecd42 100644 --- a/src/views/Profile.vue +++ b/src/views/Profile.vue @@ -41,7 +41,15 @@ Atom feed -
  • +
  • + + Verify ethereum address + +
  • +
  • -
    -
    -
    {{ field.name }}
    -
    -
    +
    +
    +
    {{ field.name }}
    +
    +
    + +
    +
    import { Options, Vue, setup } from "vue-class-component" -import { Profile, getProfile } from "@/api/users" import { Post, getProfileTimeline } from "@/api/posts" import { follow, @@ -163,6 +181,13 @@ import { isSubscriptionConfigured, makeSubscriptionPayment, } from "@/api/subscriptions" +import { + createIdentityProof, + getIdentityClaim, + getProfile, + Profile, + ProfileField, +} from "@/api/users" import Avatar from "@/components/Avatar.vue" import PostList from "@/components/PostList.vue" import ProfileListItem from "@/components/ProfileListItem.vue" @@ -170,7 +195,7 @@ import Sidebar from "@/components/Sidebar.vue" import { BACKEND_URL } from "@/constants" import { useInstanceInfo } from "@/store/instance" import { useCurrentUser } from "@/store/user" -import { getWallet } from "@/utils/ethereum" +import { getWallet, getWalletSignature } from "@/utils/ethereum" @Options({ components: { @@ -237,6 +262,13 @@ export default class ProfileView extends Vue { return this.store.getActorAddress(this.profile) } + get fields(): ProfileField[] { + if (!this.profile) { + return [] + } + return this.profile.identity_proofs.concat(this.profile.fields) + } + isCurrentUser(): boolean { if (!this.store.currentUser || !this.profile) { return false @@ -358,11 +390,27 @@ export default class ProfileView extends Vue { return `${BACKEND_URL}/feeds/${this.profile.username}` } + async verifyEthereumAddress(): Promise { + if (!this.profile || !this.isCurrentUser()) { + return + } + const signer = await getWallet() + if (!signer) { + return + } + const authToken = this.store.ensureAuthToken() + const message = await getIdentityClaim(authToken) + const signature = await getWalletSignature(signer, message) + const profile = await createIdentityProof(authToken, signature) + this.profile.identity_proofs = profile.identity_proofs + } + canConnectWallet(): boolean { return Boolean(this.store.instance?.blockchain_contract_address) && !this.walletConnected } async connectWallet() { + // Part of subscription UI const signer = await getWallet() if (!signer) { return @@ -576,32 +624,45 @@ $avatar-size: 170px; .bio { padding: 0 $block-inner-padding $block-inner-padding; white-space: pre-line; + + :deep(a) { + @include block-link; + } } .extra-fields { border-bottom: 1px solid $separator-color; margin-bottom: $block-inner-padding; - dl { + .field { + border-top: 1px solid $separator-color; display: flex; + gap: $block-inner-padding / 2; + padding: $block-inner-padding / 2 $block-inner-padding; - dt, - dd { - border-top: 1px solid $separator-color; - padding: $block-inner-padding / 2 $block-inner-padding; - } - - dt { + .name { font-weight: bold; min-width: 120px; width: 120px; } - dd { + .value { flex-grow: 1; overflow-x: hidden; text-overflow: ellipsis; } + + &.verified .value { + font-weight: bold; + } + + /* stylelint-disable-next-line selector-max-compound-selectors */ + .verified-icon img { + filter: $text-colorizer; + height: 1em; + min-width: 1em; + width: 1em; + } } }