Frontend
SignUp -> Backup
/pages/login.tsx/handleSignUp
- randomly generate
- localStorage.setItem
- setUnverifiedSecretKey
- ->
router.push("/backup")
=> backup.tsx
const handleSignUp = useCallback(() => {
console.log("generating a new secret key")
const array = new Uint8Array(32)
crypto.getRandomValues(array)
const v = Buffer.from(array).toString("hex")
localStorage.setItem(LOCAL_STORAGE_SECRET_KEY_UNVERIFIED, v)
setUnverifiedSecretKey(v)
router.push("/backup")
}, [])
Backup -> connect
一个简单的 copy secret key 的 page
pages/backup.tsx
<div className="mt-16 max-w-lg m-auto font-mono">
<div className="m-2 border border-gray-300 rounded-xl p-6">
<div>
This is your ZK CHAT login token. Keep it secret and save it somewhere
safe:
</div>
<textarea
className="block w-full outline-none py-5 px-6 my-6 rounded-xl border focus:border-blue-300 resize-none text-gray-800"
rows={3}
readOnly
value={unverifiedSecretKey || ""}
/>
<input
className="block w-full cursor-pointer bg-gray-300 text-gray-800 rounded-xl px-4 py-2 my-4"
type="button"
value={copied ? "Copied!" : "Copy"}
onClick={handleCopy}
/>
<Link href="/connect">
<a className="block cursor-pointer bg-pink hover:bg-midpink text-white text-center rounded-xl px-4 py-2">
Next
</a>
</Link>
</div>
</div>
connect -> /
用户通过发布推文来验证其密钥(或身份)的所有权
getServerSideProps
函数
- 这是 Next.js 的服务器端渲染(SSR)功能之一,用于在页面渲染之前从服务器获取必要的数据
- 通过
nookies
库,函数尝试从cookies中获取publicKey
- 如果
publicKey
存在,它会在数据库中查找相应的用户。如果用户不存在(表示未验证),则销毁publicKey
cookie;如果用户存在,则重定向到首页,因为用户已经验证过了
openTwitterIntent
函数
- 一个使用
useCallback
钩子创建的函数,用于打开Twitter发布推文的URL。
- 如果
intent
URL不为null
,则在新窗口中打开该URL,并设置waiting
状态为true
,10秒后将其设置回false
。
createUser
函数
- 另一个
useCallback
钩子创建的异步函数,用于在用户验证其密钥所有权后创建用户记录
- 函数首先检查必要的条件,如未验证的密钥、公钥是否存在,以及是否正在等待
- 通过发送POST请求到
/api/users
端点(带上公钥),尝试创建用户。如果请求成功,将用户的密钥保存到本地存储,更新应用上下文中的用户信息,并重定向到首页
- 如果请求失败(在这段代码中,失败的分支被注释掉了),理论上应该提示用户验证失败,但这里直接模拟了成功的情况
const createUser = useCallback(async () => {
if (unverifiedSecretKey === null || publicKey === null || waiting) {
return
}
console.log("CONNECT- createUser");
const res = await fetch(`/api/users?publicKey=${publicKey}`, {
method: "POST",
})
if (res.status === 200) {
const user = await res.json()
localStorage.setItem(LOCAL_STORAGE_SECRET_KEY, unverifiedSecretKey)
setSecretKey(unverifiedSecretKey)
setUser(user)
router.push("/")
}
else {
console.error("CONNECT- createUser\n" + res);
alert("Could not verify user! Did you post the tweet?")
}
}, [unverifiedSecretKey, publicKey, waiting])
这里 POST /api/users
做 2 件事
- Twitter check 确实有这个 post
- 接收公钥、
- 验证该公钥是否已经通过特定推文公开验证
- 构造一个查询字符串,用于在Twitter上搜索包含
@zkChatTwitterHandle
和publicKey
的推文
- 使用Twitter API 的
/2/tweets/search/recent
端点,发起一个GET请求来搜索最近的相关推文
- 请求头中包括
User-Agent
和使用环境变量TWITTER_BEARER_TOKEN
设置的Authorization
- 在数据库中创建用户记录,并设置cookie
- PUSH 到后端数据库
完成之后,回到首页,可以 verify
login
const handleLogin = useCallback(async () => {
console.log("loggin......Demian")
console.log("loggin in", valid, value)
if (valid) {
// if login is successful then this value will get set to
// LOCAL_STORAGE_SECRET_KEY in the first effect callback in _app.tsx
localStorage.setItem(LOCAL_STORAGE_SECRET_KEY_UNVERIFIED, value)
const publicKey = derivePublicKey(value)
// set the cookie client side and reload the page.
// if the public key is registered on the server then
// the server will redirect us to the homepage.
// if the public key is not registsered then the server
// will clear the publicKey cookie.
// setCookie(null, "publicKey", publicKey, {
// maxAge: 30 * 24 * 60 * 60,
// path: "/",
// secure: true,
// })
const res = await fetch(`/api/login?publicKey=${publicKey}`, {
method: "POST",
})
if (res.status === 200) {
const { user } = await res.json()
localStorage.setItem(LOCAL_STORAGE_SECRET_KEY, value)
setSecretKey(value)
setUser(user)
router.push("/")
// window.location.href = "/"
} else {
setUser(null)
router.push(`/login?error=${accountNotFoundError}`)
// window.location.href = `/login?error=${accountNotFoundError}`
}
}
}, [value, valid])
derivePublicKey
hash 私钥 -> 公钥
export function derivePublicKey(secretKey: string) {
const n = BigInt("0x" + secretKey)
return mimcHash(n).toString(16)
}
/api/login?publicKey
在 Backend postgres 数据库中 search 该 public key
const user = await prisma.user.findUnique({
where: { publicKey },
select: userProps,
})
if (user !== null) {
nookies.set({ res }, "publicKey", user.publicKey, {
maxAge: 30 * 24 * 60 * 60,
path: "/",
httpOnly: true,
})
return res.status(200).json({ user })
} else {
return res.status(404).end()
}
prove:
CreateMessage ????
export function CreateMessage(props: CreateMessageProps) {
const { user, secretKey } = useContext(PageContext)
const [value, setValue] = useState("")
const router = useRouter()
const handleSubmit = useCallback(async () => {
if (secretKey === null) {
return
}
const hash = hashMessage(value).toString(16)
const { proof, publicSignals } = await sign(secretKey, props.group, value)
这个 value 就是你 input 的值:
<input
className={
isUserInGroup
? "rounded-lg px-4 py-3 flex-1 bg-white"
: "rounded-lg px-4 py-3 flex-1 bg-white placeholder-light"
}
disabled={!isUserInGroup}
type="text"
placeholder={
isUserInGroup
? "Reply"
: "You must be logged in as a group member to reply"
}
value={value}
onChange={(event) => setValue(event.target.value)}
/>
verify
export async function verifyMessage(
vKeys: VKeys,
message: Message
): Promise<boolean> {
const verified = await snarkjs.groth16.verify(
vKeys.sign,
message.publicSignals,
message.proof
)