Skip to content
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
4 changes: 4 additions & 0 deletions .commandcode/taste/taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Taste (Continuously Learned by [CommandCode][cmd])

[cmd]: https://commandcode.ai/

6 changes: 5 additions & 1 deletion .github/workflows/deploy-cloudflare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ jobs:
SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
SUPABASE_URL=${SUPABASE_URL}
WEB_URL=${WEB_URL}
NODE_ENV=production
EOF
- name: Deploy to Cloudflare Workers
- name: Deploy Frontend (tera-web) to Cloudflare Workers
run: pnpm exec wrangler deploy --config wrangler.jsonc --secrets-file .cloudflare.secrets.env

- name: Deploy Backend API (tera-api) to Cloudflare Workers
run: pnpm exec wrangler deploy --config backend-server/wrangler.toml --secrets-file .cloudflare.secrets.env
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node-linker=hoisted
5 changes: 4 additions & 1 deletion backend-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"googleapis": "^144.0.0",
"hono": "^4.12.21",
"jose": "^5.9.6",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2"
},
Expand All @@ -31,6 +33,7 @@
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20.14.14",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
"typescript": "^5.5.4",
"wrangler": "^4.93.0"
}
}
56 changes: 56 additions & 0 deletions backend-server/src/middleware/hono-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { jwtVerify } from 'jose';

function getSecret(): Uint8Array {
const secret = process.env.SUPABASE_JWT_SECRET;
if (!secret) throw new Error('SUPABASE_JWT_SECRET not configured');
return new TextEncoder().encode(secret);
}

export async function honoAuthMiddleware(c: any, next: any) {
try {
const authHeader = c.req.header('Authorization');
const token = authHeader?.replace('Bearer ', '');

if (!token) {
return c.json({
success: false,
error: 'No authorization token provided',
}, 401);
}

try {
const { payload } = await jwtVerify(token, getSecret());
c.set('user', payload);
await next();
} catch {
return c.json({
success: false,
error: 'Invalid or expired token',
}, 401);
}
} catch (error) {
return c.json({
success: false,
error: 'Authentication error',
}, 500);
}
}

export async function honoOptionalAuth(c: any, next: any) {
try {
const authHeader = c.req.header('Authorization');
const token = authHeader?.replace('Bearer ', '');

if (token) {
try {
const { payload } = await jwtVerify(token, getSecret());
c.set('user', payload);
} catch {
// Silently fail
}
}
await next();
} catch {
await next();
}
}
195 changes: 195 additions & 0 deletions backend-server/src/routes/hono-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { Hono } from 'hono';
import { supabase } from '../services/supabase.js';

const router = new Hono();

// Google OAuth callback handler
router.post('/google', async (c: any) => {
try {
const { idToken } = await c.req.json();

if (!idToken) {
return c.json({
success: false,
error: 'idToken is required',
}, 400);
}

// Sign in with Google ID token via Supabase
const { data, error } = await supabase.auth.signInWithIdToken({
provider: 'google',
token: idToken,
});

if (error) {
console.error('Auth error:', error);
return c.json({
success: false,
error: 'Authentication failed',
}, 401);
}

if (!data.user || !data.session) {
return c.json({
success: false,
error: 'Authentication failed: No user or session',
}, 401);
}

return c.json({
success: true,
data: {
token: data.session.access_token,
user: {
id: data.user.id,
email: data.user.email,
name: data.user.user_metadata?.name || '',
provider: 'google',
},
},
});
} catch (error) {
console.error('Google auth error:', error);
return c.json({
success: false,
error: 'Authentication error',
}, 500);
}
});

// Email/password sign in
router.post('/signin', async (c: any) => {
try {
const { email, password } = await c.req.json();

if (!email || !password) {
return c.json({
success: false,
error: 'Email and password are required',
}, 400);
}

const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});

if (error) {
console.error('Sign in error:', error);
return c.json({
success: false,
error: 'Invalid email or password',
}, 401);
}

if (!data.user || !data.session) {
return c.json({
success: false,
error: 'Sign in failed',
}, 401);
}

return c.json({
success: true,
data: {
token: data.session.access_token,
user: {
id: data.user.id,
email: data.user.email,
name: data.user.user_metadata?.name || '',
provider: 'email',
},
},
});
} catch (error) {
console.error('Sign in error:', error);
return c.json({
success: false,
error: 'Sign in failed',
}, 500);
}
});

// Email/password sign up
router.post('/signup', async (c: any) => {
try {
const { email, password, name } = await c.req.json();

if (!email || !password) {
return c.json({
success: false,
error: 'Email and password are required',
}, 400);
}

const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
name: name || email.split('@')[0],
},
},
});

if (error) {
console.error('Sign up error:', error);
return c.json({
success: false,
error: error.message || 'Sign up failed',
}, 400);
}

if (!data.user) {
return c.json({
success: false,
error: 'Sign up failed',
}, 400);
}

return c.json({
success: true,
data: {
user: {
id: data.user.id,
email: data.user.email,
name: name || email.split('@')[0],
provider: 'email',
},
message: 'Sign up successful. Please check your email to confirm your account.',
},
});
} catch (error) {
console.error('Sign up error:', error);
return c.json({
success: false,
error: 'Sign up failed',
}, 500);
}
});

// Sign out
router.post('/signout', async (c: any) => {
try {
const { error } = await supabase.auth.signOut();

if (error) {
return c.json({
success: false,
error: 'Sign out failed',
}, 400);
}

return c.json({
success: true,
message: 'Signed out successfully',
});
} catch (error) {
return c.json({
success: false,
error: 'Sign out failed',
}, 500);
}
});

export default router;
Loading