Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Feature/guest user admin #199

Merged
merged 14 commits into from
Apr 25, 2024
Merged
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: "[email protected]",
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
Loading