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 files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
!.env.local.example
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.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
|
5. Website calls `POST /api/v1/auth/website/challenge` with origin
|
||||||
6. Backend returns challenge message (with nonce + timestamp + origin)
|
6. Backend returns challenge message (with nonce + timestamp + origin)
|
||||||
7. Website calls `window.smirk.signMessage(challenge)`
|
7. Website calls `window.smirk.signMessage(challenge)`
|
||||||
8. Extension shows signing popup with the message
|
8. Extension shows asset picker - user chooses favorite coin
|
||||||
9. User approves → Extension returns signatures from all 5 keys
|
9. User signs → Extension returns signature from chosen asset
|
||||||
10. Website sends `{ challenge_id, signatures }` to `POST /api/v1/auth/website/verify`
|
10. Website sends `{ challenge_id, signature }` to `POST /api/v1/auth/website/verify`
|
||||||
11. Backend verifies signatures, looks up user by BTC pubkey_hash, issues JWT session
|
11. Backend verifies signature, looks up user by pubkey_hash, issues JWT session
|
||||||
12. Website stores JWT, user is now authenticated
|
12. Website stores JWT, user is now authenticated
|
||||||
|
|
||||||
**Future**: Allow single-asset auth (user picks their favorite coin to sign with).
|
|
||||||
|
|
||||||
### Tech Stack
|
### Tech Stack
|
||||||
|
|
||||||
- **Framework**: Next.js 14+ (App Router)
|
- **Framework**: Next.js 14+ (App Router)
|
||||||
|
|||||||
@@ -134,14 +134,11 @@ Origin: https://smirk.cash
|
|||||||
|
|
||||||
Challenge should expire after ~5 minutes to prevent replay attacks.
|
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:
|
This positions Smirk as infrastructure for wallet-based authentication across multiple cryptocurrencies.
|
||||||
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".
|
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
@import "tailwindcss";
|
@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 {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: #0a0a0a;
|
||||||
--foreground: #ededed;
|
--foreground: #ededed;
|
||||||
@@ -73,3 +81,13 @@ body {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
+18
-11
@@ -60,14 +60,17 @@ export default function Home() {
|
|||||||
const challenge = await getChallenge(window.location.origin);
|
const challenge = await getChallenge(window.location.origin);
|
||||||
|
|
||||||
// Sign with the selected coin
|
// Sign with the selected coin
|
||||||
const signatures = await window.smirk.signMessage(challenge.challenge);
|
const result = await window.smirk.signMessage(challenge.challenge);
|
||||||
const sig = signatures[coin];
|
const sig = result.signatures.find((s) => s.asset === coin);
|
||||||
|
if (!sig) {
|
||||||
|
throw new Error(`No signature found for ${coin}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Verify with backend
|
// Verify with backend
|
||||||
const auth = await verifySignature(challenge.challenge_id, {
|
const auth = await verifySignature(challenge.challenge_id, {
|
||||||
asset: coin,
|
asset: coin,
|
||||||
signature: sig.signature,
|
signature: sig.signature,
|
||||||
public_key: sig.public_key,
|
public_key: sig.publicKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store token and show logged in
|
// Store token and show logged in
|
||||||
@@ -101,14 +104,18 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="mb-12 logo-glow">
|
<div className="mb-12 flex flex-col items-center">
|
||||||
<Image
|
<h1 className="creepster-text text-4xl mb-4">SMIRK WALLET</h1>
|
||||||
src="/logo_w_creepytext.svg"
|
<div className="logo-glow">
|
||||||
alt="Smirk Wallet"
|
<Image
|
||||||
width={220}
|
src="/logo.svg"
|
||||||
height={200}
|
alt="Smirk"
|
||||||
priority
|
width={160}
|
||||||
/>
|
height={160}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="creepster-text text-2xl mt-4">smirk.ca$h</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Initial state - Connect button */}
|
{/* Initial state - Connect button */}
|
||||||
|
|||||||
+4
-4
@@ -34,8 +34,8 @@ export async function getChallenge(origin: string): Promise<ChallengeResponse> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.json().catch(() => ({ message: 'Request failed' }));
|
const data = await res.json().catch(() => ({}));
|
||||||
throw new Error(error.message || 'Failed to get challenge');
|
throw new Error(data.error || data.message || 'Failed to get challenge');
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json();
|
return res.json();
|
||||||
@@ -55,8 +55,8 @@ export async function verifySignature(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.json().catch(() => ({ message: 'Request failed' }));
|
const data = await res.json().catch(() => ({}));
|
||||||
throw new Error(error.message || 'Failed to verify signature');
|
throw new Error(data.error || data.message || 'Failed to verify signature');
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|||||||
Vendored
+7
-9
@@ -9,25 +9,23 @@ interface SmirkPublicKeys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SmirkSignature {
|
interface SmirkSignature {
|
||||||
asset: string;
|
asset: 'btc' | 'ltc' | 'xmr' | 'wow' | 'grin';
|
||||||
signature: string;
|
signature: string;
|
||||||
public_key: string;
|
publicKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SmirkSignatures {
|
interface SmirkSignResult {
|
||||||
btc: SmirkSignature;
|
message: string;
|
||||||
ltc: SmirkSignature;
|
signatures: SmirkSignature[];
|
||||||
xmr: SmirkSignature;
|
|
||||||
wow: SmirkSignature;
|
|
||||||
grin: SmirkSignature;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SmirkAPI {
|
interface SmirkAPI {
|
||||||
|
isSmirk: boolean;
|
||||||
connect(): Promise<SmirkPublicKeys>;
|
connect(): Promise<SmirkPublicKeys>;
|
||||||
disconnect(): Promise<void>;
|
disconnect(): Promise<void>;
|
||||||
isConnected(): Promise<boolean>;
|
isConnected(): Promise<boolean>;
|
||||||
getPublicKeys(): Promise<SmirkPublicKeys | null>;
|
getPublicKeys(): Promise<SmirkPublicKeys | null>;
|
||||||
signMessage(message: string): Promise<SmirkSignatures>;
|
signMessage(message: string): Promise<SmirkSignResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
Reference in New Issue
Block a user