mirror of
https://github.com/Such-Software/smirk-website.git
synced 2026-10-17 05:51:28 +02:00
feat: single-asset auth, UI improvements
- Auth flow now uses single signature from user's chosen coin - Update SmirkSignResult types to match extension API - Add Creepster font for branding - Better error messages from API responses - Update docs to reflect single-asset auth is working
This commit is contained in:
@@ -32,6 +32,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.local.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -52,14 +52,12 @@ The Smirk extension injects a `window.smirk` API object into web pages. The webs
|
||||
5. Website calls `POST /api/v1/auth/website/challenge` with origin
|
||||
6. Backend returns challenge message (with nonce + timestamp + origin)
|
||||
7. Website calls `window.smirk.signMessage(challenge)`
|
||||
8. Extension shows signing popup with the message
|
||||
9. User approves → Extension returns signatures from all 5 keys
|
||||
10. Website sends `{ challenge_id, signatures }` to `POST /api/v1/auth/website/verify`
|
||||
11. Backend verifies signatures, looks up user by BTC pubkey_hash, issues JWT session
|
||||
8. Extension shows asset picker - user chooses favorite coin
|
||||
9. User signs → Extension returns signature from chosen asset
|
||||
10. Website sends `{ challenge_id, signature }` to `POST /api/v1/auth/website/verify`
|
||||
11. Backend verifies signature, looks up user by pubkey_hash, issues JWT session
|
||||
12. Website stores JWT, user is now authenticated
|
||||
|
||||
**Future**: Allow single-asset auth (user picks their favorite coin to sign with).
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- **Framework**: Next.js 14+ (App Router)
|
||||
|
||||
@@ -134,14 +134,11 @@ Origin: https://smirk.cash
|
||||
|
||||
Challenge should expire after ~5 minutes to prevent replay attacks.
|
||||
|
||||
### Single-Asset Authentication (Future Enhancement)
|
||||
### Single-Asset Authentication (DONE - 2026-01-25)
|
||||
|
||||
**Current implementation**: Backend requires signatures from all 5 asset keys.
|
||||
User picks their favorite coin to sign with:
|
||||
- Simpler UX - choose one coin, sign once
|
||||
- User data - "What's your favorite coin?" insight
|
||||
- Any of the 5 coins proves wallet ownership
|
||||
|
||||
**Better approach**: Let users pick their favorite coin to sign with. Benefits:
|
||||
1. **Simpler UX** - User chooses one coin, signs once
|
||||
2. **User data** - "What's your favorite coin?" gives us insight into our user base
|
||||
3. **Proof of concept** - Demonstrates wallet-based auth for ANY of these 5 coins
|
||||
4. **Still secure** - We already know all 5 pubkeys; signing with ANY one proves wallet ownership
|
||||
|
||||
This positions Smirk as infrastructure for wallet-based authentication across multiple cryptocurrencies. Other websites could use the same pattern - "Sign in with Bitcoin/Litecoin/Monero/Wownero/Grin".
|
||||
This positions Smirk as infrastructure for wallet-based authentication across multiple cryptocurrencies.
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Creepster';
|
||||
src: url('/fonts/Creepster-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
@@ -73,3 +81,13 @@ body {
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Creepster text styling */
|
||||
.creepster-text {
|
||||
font-family: 'Creepster', cursive;
|
||||
color: #fbeb0a;
|
||||
text-shadow:
|
||||
0 0 10px rgba(251, 235, 10, 0.6),
|
||||
0 0 20px rgba(251, 235, 10, 0.4),
|
||||
0 0 30px rgba(251, 235, 10, 0.2);
|
||||
}
|
||||
|
||||
+15
-8
@@ -60,14 +60,17 @@ export default function Home() {
|
||||
const challenge = await getChallenge(window.location.origin);
|
||||
|
||||
// Sign with the selected coin
|
||||
const signatures = await window.smirk.signMessage(challenge.challenge);
|
||||
const sig = signatures[coin];
|
||||
const result = await window.smirk.signMessage(challenge.challenge);
|
||||
const sig = result.signatures.find((s) => s.asset === coin);
|
||||
if (!sig) {
|
||||
throw new Error(`No signature found for ${coin}`);
|
||||
}
|
||||
|
||||
// Verify with backend
|
||||
const auth = await verifySignature(challenge.challenge_id, {
|
||||
asset: coin,
|
||||
signature: sig.signature,
|
||||
public_key: sig.public_key,
|
||||
public_key: sig.publicKey,
|
||||
});
|
||||
|
||||
// Store token and show logged in
|
||||
@@ -101,15 +104,19 @@ export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
{/* Logo */}
|
||||
<div className="mb-12 logo-glow">
|
||||
<div className="mb-12 flex flex-col items-center">
|
||||
<h1 className="creepster-text text-4xl mb-4">SMIRK WALLET</h1>
|
||||
<div className="logo-glow">
|
||||
<Image
|
||||
src="/logo_w_creepytext.svg"
|
||||
alt="Smirk Wallet"
|
||||
width={220}
|
||||
height={200}
|
||||
src="/logo.svg"
|
||||
alt="Smirk"
|
||||
width={160}
|
||||
height={160}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<p className="creepster-text text-2xl mt-4">smirk.ca$h</p>
|
||||
</div>
|
||||
|
||||
{/* Initial state - Connect button */}
|
||||
{step === 'initial' && (
|
||||
|
||||
+4
-4
@@ -34,8 +34,8 @@ export async function getChallenge(origin: string): Promise<ChallengeResponse> {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: 'Request failed' }));
|
||||
throw new Error(error.message || 'Failed to get challenge');
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || data.message || 'Failed to get challenge');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
@@ -55,8 +55,8 @@ export async function verifySignature(
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: 'Request failed' }));
|
||||
throw new Error(error.message || 'Failed to verify signature');
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || data.message || 'Failed to verify signature');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
|
||||
Vendored
+7
-9
@@ -9,25 +9,23 @@ interface SmirkPublicKeys {
|
||||
}
|
||||
|
||||
interface SmirkSignature {
|
||||
asset: string;
|
||||
asset: 'btc' | 'ltc' | 'xmr' | 'wow' | 'grin';
|
||||
signature: string;
|
||||
public_key: string;
|
||||
publicKey: string;
|
||||
}
|
||||
|
||||
interface SmirkSignatures {
|
||||
btc: SmirkSignature;
|
||||
ltc: SmirkSignature;
|
||||
xmr: SmirkSignature;
|
||||
wow: SmirkSignature;
|
||||
grin: SmirkSignature;
|
||||
interface SmirkSignResult {
|
||||
message: string;
|
||||
signatures: SmirkSignature[];
|
||||
}
|
||||
|
||||
interface SmirkAPI {
|
||||
isSmirk: boolean;
|
||||
connect(): Promise<SmirkPublicKeys>;
|
||||
disconnect(): Promise<void>;
|
||||
isConnected(): Promise<boolean>;
|
||||
getPublicKeys(): Promise<SmirkPublicKeys | null>;
|
||||
signMessage(message: string): Promise<SmirkSignatures>;
|
||||
signMessage(message: string): Promise<SmirkSignResult>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
||||
Reference in New Issue
Block a user