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
6 changes: 6 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
MONGODB_CONNECTION_STRING=mongodb://127.0.0.1:27017/mongoose_test
STRIPE_SECRET_KEY=sk_test_123
STRIPE_WEBHOOK_SECRET=whsec_test_123
GITHUB_REDIRECT_URI=http://localhost:7777/oauth-github-login
MAILGUN_FROM_EMAIL=noreply@example.com
MAILGUN_DOMAIN=example.com
MAILGUN_PRIVATE_KEY=key-test
5 changes: 5 additions & 0 deletions netlify/functions/getMyAccount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const extrovert = require('extrovert');

module.exports = extrovert.toNetlifyFunction(require('../../src/actions/getMyAccount'));
19 changes: 16 additions & 3 deletions netlify/functions/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ module.exports = extrovert.toNetlifyFunction(async function github(params) {
const { access_token: token } = await githubOAuth.getAccessToken(code);
const userData = await githubOAuth.getUser(token);

const { id: githubUserId, notification_email: email, avatar_url: picture, name, login: githubUsername } = userData;
const { id: githubUserId, notification_email: notificationEmail, avatar_url: picture, name, login: githubUsername } = userData;
const email = notificationEmail || await githubOAuth.getPrimaryEmail(token) || `${githubUsername}@users.noreply.github.com`;
Comment thread
vkarpov15 marked this conversation as resolved.

const $set = {
githubUserId,
Expand All @@ -55,9 +56,12 @@ module.exports = extrovert.toNetlifyFunction(async function github(params) {
let roles = null;
if (member == null) {
const invitation = await Invitation.findOne({
githubUsername,
workspaceId,
status: 'pending'
status: 'pending',
$or: [
{ githubUsername },
{ email: email.toLowerCase() }
Comment thread
vkarpov15 marked this conversation as resolved.
]
});
if (invitation != null) {
workspace.members.push({ userId: user._id, roles: invitation.roles });
Expand All @@ -73,6 +77,15 @@ module.exports = extrovert.toNetlifyFunction(async function github(params) {
const seats = users.length;
await stripe.updateSubscriptionSeats(workspace.stripeSubscriptionId, seats);
}
} else if (
workspace.members.length === 0 &&
workspace.stripeCustomerEmail &&
workspace.stripeCustomerEmail.toLowerCase() === email.toLowerCase()
) {
workspace.members.push({ userId: user._id, roles: ['owner'] });
workspace.stripeCustomerEmail = null;
await workspace.save();
roles = ['owner'];
Comment thread
vkarpov15 marked this conversation as resolved.
} else if (workspace.subscriptionTier === 'free') {
workspace.members.push({ userId: user._id, roles: ['readonly'] });
await workspace.save();
Expand Down
8 changes: 8 additions & 0 deletions netlify/functions/google.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ module.exports = extrovert.toNetlifyFunction(async function googleLogin(params)
const seats = users.length;
await stripe.updateSubscriptionSeats(workspace.stripeSubscriptionId, seats);
}
} else if (
workspace.members.length === 0 &&
workspace.stripeCustomerEmail &&
workspace.stripeCustomerEmail.toLowerCase() === email.toLowerCase()
) {
workspace.members.push({ userId: user._id, roles: ['owner'] });
await workspace.save();
roles = ['owner'];
Comment thread
vkarpov15 marked this conversation as resolved.
} else if (workspace.subscriptionTier === 'free') {
workspace.members.push({ userId: user._id, roles: ['readonly'] });
await workspace.save();
Expand Down
5 changes: 5 additions & 0 deletions netlify/functions/updateWorkspace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const extrovert = require('extrovert');

module.exports = extrovert.toNetlifyFunction(require('../../src/actions/updateWorkspace'));
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
"cheerio": "1.0.0",
"extrovert": "0.2.0",
"googleapis": "100.0.0",
"mongoose": "^9.0.0-0",
"mongoose": "9.x",
Comment thread
vkarpov15 marked this conversation as resolved.
"ramda": "0.28.0",
"random-word-slugs": "^0.1.7",
"stripe": "9.9.0",
"time-commando": "1.0.1",
"vue": "3.x"
"vue": "3.x",
"xss": "^1.0.15"
},
"devDependencies": {
"@masteringjs/eslint-config": "0.1.1",
Expand Down
157 changes: 157 additions & 0 deletions public/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mongoose Studio Account Setup</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gradient-to-br from-slate-100 via-white to-slate-200 text-slate-900">
<main class="mx-auto flex min-h-screen w-full max-w-xl items-center px-4">
<section class="w-full rounded-2xl border border-slate-200 bg-white p-8 shadow-xl shadow-slate-200/70">
<h1 class="text-3xl font-bold tracking-tight">Set up your account</h1>
<p id="message" class="mt-2 text-slate-600">Sign in to connect your account and access your API key.</p>

<div class="mt-6 grid grid-cols-2 gap-3">
<button id="githubButton" type="button" class="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-3 text-sm font-semibold text-slate-800 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60">
<svg viewBox="0 0 98 98" class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#111827"/></svg>
Continue with GitHub
</button>
<button id="googleButton" type="button" class="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-3 text-sm font-semibold text-slate-800 transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" aria-hidden="true"><path fill="#EA4335" d="M256 212v88h146c-6 38-22 66-47 85l76 59c44-40 69-99 69-169 0-16-1-28-4-40z"/><path fill="#34A853" d="M256 512c68 0 126-22 168-59l-76-59c-21 14-49 24-92 24-70 0-129-47-150-111l-79 61c41 81 125 144 229 144z"/><path fill="#4A90E2" d="M106 307c-5-14-8-30-8-47s3-33 8-47l-79-61C10 185 0 221 0 260s10 75 27 108z"/><path fill="#FBBC05" d="M256 102c37 0 70 13 96 38l72-72C382 28 324 0 256 0 152 0 68 63 27 152l79 61c21-64 80-111 150-111z"/></svg>
Continue with Google
</button>
</div>

<p class="mt-4">
<a href="https://discord.gg/P3YCfKYxpy" target="_blank" rel="noopener noreferrer" class="text-sm font-semibold text-[#880000] underline decoration-[#880000]/40 underline-offset-2">Get Help on Discord</a>
</p>

<p id="error" class="mt-4 text-sm font-semibold text-red-700" role="alert"></p>
<p class="mt-4 text-sm text-slate-600">Already signed in? <a href="/my-account.html" class="font-semibold text-[#880000] underline decoration-[#880000]/40 underline-offset-2">Go to My Account</a></p>
</section>
</main>

<script>
(function() {
const ACCESS_TOKEN_KEY = '_mongooseStudioAccessToken';
const WORKSPACE_KEY = '_mongooseStudioSetupWorkspaceId';

const githubButton = document.getElementById('githubButton');
const googleButton = document.getElementById('googleButton');
const errorEl = document.getElementById('error');
const messageEl = document.getElementById('message');

const params = new URLSearchParams(window.location.search);
const workspaceIdFromQuery = params.get('workspaceId');
const provider = params.get('provider');
const code = params.get('code');

if (workspaceIdFromQuery) {
sessionStorage.setItem(WORKSPACE_KEY, workspaceIdFromQuery);
}

const workspaceId = workspaceIdFromQuery || sessionStorage.getItem(WORKSPACE_KEY);
if (!workspaceId) {
messageEl.textContent = 'Missing workspace information. Open the setup link from your email.';
githubButton.disabled = true;
googleButton.disabled = true;
}

if (provider && code) {
exchangeOAuthCode(provider, code, workspaceId);
}

githubButton.addEventListener('click', function() {
beginLogin('githubLogin');
});

googleButton.addEventListener('click', function() {
beginLogin('googleLogin');
});

async function beginLogin(endpoint) {
if (!workspaceId) {
return showError('Missing workspace ID. Use the setup link from your email.');
}
try {
setLoading(true);
const state = window.location.origin + window.location.pathname;
const response = await postJson('/.netlify/functions/' + endpoint, { state });
if (!response.url) {
throw new Error('Login URL not returned');
}
window.location.href = response.url;
} catch (err) {
showError(err.message || 'Failed to start login');
} finally {
setLoading(false);
}
}

async function exchangeOAuthCode(provider, oauthCode, savedWorkspaceId) {
if (!savedWorkspaceId) {
return showError('Missing workspace ID. Please restart account setup from your email link.');
}

try {
setLoading(true);
messageEl.textContent = 'Finishing sign in...';
const endpoint = provider === 'google' ? 'google' : 'github';
const data = await postJson('/.netlify/functions/' + endpoint, {
code: oauthCode,
workspaceId: savedWorkspaceId
});

if (!data || !data.accessToken || !data.accessToken._id) {
throw new Error('Login did not return an access token');
}
if (!data.roles || data.roles.length === 0) {
throw new Error('Your account is not authorized for this workspace');
}

localStorage.setItem(ACCESS_TOKEN_KEY, data.accessToken._id);
window.location.replace('/my-account.html');
} catch (err) {
showError(err.message || 'Login failed');
} finally {
setLoading(false);
}
}

async function postJson(url, body) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body || {})
});

const text = await res.text();
let data = {};
if (text) {
try {
data = JSON.parse(text);
} catch (err) {
data = { message: text };
}
}

if (!res.ok) {
throw new Error(data.message || 'Request failed');
}

return data;
}

function setLoading(loading) {
githubButton.disabled = loading;
googleButton.disabled = loading;
}

function showError(message) {
errorEl.textContent = message;
}
})();
</script>
</body>
</html>
Loading