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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ npm run dev

Finally, open the application on https://dev.local:3000.

## Database Schemas

See `create-tables.sql`

## 🚀 Deploying the App

The app is hosted on Heroku in two different environments.
Expand Down
8 changes: 7 additions & 1 deletion create-tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,14 @@ CREATE TABLE access_tokens
provider VARCHAR(255) NOT NULL,
provider_account_id VARCHAR(255) NOT NULL,
access_token VARCHAR(255) NOT NULL,
refresh_token VARCHAR(255) NOT NULL,
refresh_token VARCHAR(255) NULL,
last_updated_at timestamptz NOT NULL DEFAULT now(),

PRIMARY KEY (provider, provider_account_id)
);

CREATE TABLE guests (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
projects jsonb
);
1 change: 1 addition & 0 deletions drop-tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ DROP TABLE verification_token;
DROP TABLE accounts;
DROP TABLE sessions;
DROP TABLE users;
DROP TABLE guests;
35 changes: 28 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"test": "jest"
},
"dependencies": {
"@auth/pg-adapter": "^0.4.1",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
Expand All @@ -33,8 +34,10 @@
"mobx": "^6.12.0",
"next": "14.0.2",
"next-auth": "^4.24.5",
"nodemailer": "^6.9.9",
"npm": "^10.2.4",
"octokit": "^3.1.2",
"pg": "^8.11.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"redis-semaphore": "^5.5.0",
Expand All @@ -50,6 +53,7 @@
"@auth/pg-adapter": "^0.5.2",
"@types/jest": "^29.5.8",
"@types/node": "^20.9.2",
"@types/nodemailer": "^6.4.14",
"@types/pg": "^8.11.0",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
7 changes: 7 additions & 0 deletions src/app/admin/guests/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import AdminPage from "@/features/admin/view/AdminPage";

export default async function Page() {
return (
<AdminPage />
)
}
107 changes: 95 additions & 12 deletions src/composition.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Pool } from "pg"
import { NextAuthOptions } from "next-auth"
import GithubProvider from "next-auth/providers/github"
import EmailProvider from "next-auth/providers/email"
import PostgresAdapter from "@auth/pg-adapter"
import { Adapter } from "next-auth/adapters"
import RedisKeyedMutexFactory from "@/common/mutex/RedisKeyedMutexFactory"
Expand All @@ -24,7 +25,8 @@ import {
import {
GitHubOAuthTokenRefresher,
AuthjsOAuthTokenRepository,
AuthjsRepositoryAccessReader
AuthjsRepositoryAccessReader,
GitHubInstallationAccessTokenDataSource
} from "@/features/auth/data"
import {
AccessTokenRefresher,
Expand All @@ -38,6 +40,9 @@ import {
TransferringAccessTokenReader,
UserDataCleanUpLogOutHandler
} from "@/features/auth/domain"
import { IGuestInviter } from "./features/admin/domain/IGuestInviter"
import { createTransport } from "nodemailer"
import DbGuestRepository from "./features/admin/data/DbGuestRepository"

const {
GITHUB_APP_ID,
Expand All @@ -51,6 +56,16 @@ const {
POSTGRESQL_DB
} = process.env

const gitHubAppCredentials = {
appId: GITHUB_APP_ID,
clientId: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
privateKey: Buffer
.from(GITHUB_PRIVATE_KEY_BASE_64, "base64")
.toString("utf-8"),
organization: GITHUB_ORGANIZATION_NAME,
}

const pool = new Pool({
host: POSTGRESQL_HOST,
user: POSTGRESQL_USER,
Expand All @@ -67,6 +82,8 @@ const logInHandler = new OAuthAccountCredentialPersistingLogInHandler({
provider: "github"
})

const gitHubInstallationAccessTokenDataSource = new GitHubInstallationAccessTokenDataSource(gitHubAppCredentials)

export const authOptions: NextAuthOptions = {
adapter: PostgresAdapter(pool) as Adapter,
secret: process.env.NEXTAUTH_SECRET,
Expand All @@ -79,13 +96,49 @@ export const authOptions: NextAuthOptions = {
scope: "repo"
}
}
})
}),
EmailProvider({
sendVerificationRequest: ({ url }) => {
console.log("Magic link", url) // print to console for now
},
}),
],
session: {
strategy: "database"
},
callbacks: {
async signIn({ user, account }) {
async signIn({ user, account, email }) {
if (email && email.verificationRequest && user.email) { // in verification request flow
const guest = await guestRepository.findByEmail(user.email)
if (guest == undefined) {
return false // email not invited
}
}
else if (account?.provider == "email" && user.email) { // in sign in flow, click on magic link
// obtain access token from GitHub using app auth
const guest = await guestRepository.findByEmail(user.email)
if (guest == undefined) {
return false // email not invited
}
const accessToken = await gitHubInstallationAccessTokenDataSource.getAccessToken(guest.projects || [])
const query = `
INSERT INTO access_tokens (
provider,
provider_account_id,
access_token
)
VALUES ($1, $2, $3)
ON CONFLICT (provider, provider_account_id)
DO UPDATE SET access_token = $3, last_updated_at = NOW();
`
await pool.query(query, [
account.provider,
account.providerAccountId,
accessToken,
])
return true
}

if (account) {
return await logInHandler.handleLogIn(user.id, account)
} else {
Expand All @@ -99,15 +152,6 @@ export const authOptions: NextAuthOptions = {
}
}

const gitHubAppCredentials = {
appId: GITHUB_APP_ID,
clientId: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
privateKey: Buffer
.from(GITHUB_PRIVATE_KEY_BASE_64, "base64")
.toString("utf-8")
}

export const session: ISession = new AuthjsSession({ authOptions })

const accessTokenReader = new TransferringAccessTokenReader({
Expand Down Expand Up @@ -175,3 +219,42 @@ export const logOutHandler = new ErrorIgnoringLogOutHandler(
new UserDataCleanUpLogOutHandler(session, projectUserDataRepository)
])
)

export const guestRepository: IGuestRepository = new DbGuestRepository(pool)

const transport = createTransport({
host: "sandbox.smtp.mailtrap.io",
port: 2525,
auth: {
user: "0682027d57d0db",
pass: "28c3dbfbfc0af8"
}
});

export const guestInviter: IGuestInviter = {
inviteGuestByEmail: function (email: string): Promise<void> {
transport.sendMail({
to: email,
from: "no-reply@docs.shapetools.io",
subject: "You have been invited to join Shape Docs",
html: `
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<p>You have been invited to join Shape Docs!</p>
<p>Shape Docs uses magic links for authentication. This means that you don't need to remember a password.</p>
<p>Click the link below to request your first magic link to log in:</p>
<a href="https://docs.shapetools.io">Log in</a>
</body>
</html>
`,
})
return Promise.resolve()
}
}
Loading