Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
49 changes: 22 additions & 27 deletions commands/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import * as y from "yoctocolors"
import open from "open"
import clipboard from "clipboardy"
import { select, input, password } from "@inquirer/prompts"
import { select } from "@inquirer/prompts"
import { requireFramework } from "../lib/detect.js"
import { updateEnvFile } from "../lib/write-env.js"
import { providers, frameworks } from "../lib/meta.js"
import { secret } from "./index.js"
import { link, markdownToAnsi } from "../lib/markdown.js"
import { appleGenSecret } from "../lib/apple-gen-secret.js"
import { ensureAuthSecretExist } from "../lib/ensure-auth-secret-exist.js"
import { promptInput, promptPassword } from "../lib/inquirer-prompts.js"

const choices = Object.entries(providers)
.filter(([, { setupUrl }]) => !!setupUrl)
Expand Down Expand Up @@ -78,35 +80,28 @@ ${y.bold("Callback URL (copied to clipboard)")}: ${url}`

await open(provider.setupUrl)

const clientId = await input({
message: `Paste ${y.magenta("Client ID")}:`,
validate: (value) => !!value,
})
const clientSecret = await password({
message: `Paste ${y.magenta("Client secret")}:`,
mask: true,
validate: (value) => !!value,
})
if (providerId === "apple") {
const clientId = await promptInput("Client ID")
const keyId = await promptInput("Key ID")
const teamId = await promptInput("Team ID")

console.log(y.dim(`Updating environment variable file...`))
console.log(y.dim(`Updating environment variable file...`))

const varPrefix = `AUTH_${providerId.toUpperCase()}`
await appleGenSecret({ teamId, clientId, keyId })
await ensureAuthSecretExist()
} else {
const clientId = await promptInput("Client ID")
const clientSecret = await promptPassword("Client Secret")

await updateEnvFile({
[`${varPrefix}_ID`]: clientId,
[`${varPrefix}_SECRET`]: clientSecret,
})
console.log(y.dim(`Updating environment variable file...`))

console.log(
y.dim(
`\nEnsuring that ${link(
"AUTH_SECRET",
"https://authjs.dev/getting-started/installation#setup-environment"
)} is set...`
)
)

await secret.action({})
const varPrefix = `AUTH_${providerId.toUpperCase()}`
await updateEnvFile({
[`${varPrefix}_ID`]: clientId,
[`${varPrefix}_SECRET`]: clientSecret,
})
await ensureAuthSecretExist()
}

console.log("\n🎉 Done! You can now use this provider in your app.")
} catch (error) {
Expand Down
82 changes: 82 additions & 0 deletions lib/apple-gen-secret.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { SignJWT } from "jose"
import { createPrivateKey } from "crypto"
import { join } from "node:path"
import { readFile, writeFile } from "node:fs/promises"
import { updateEnvFile } from "./write-env.js"
import { detectFramework } from "./detect.js"
import { frameworks } from "./meta.js"
import fs from "node:fs"
import * as y from "yoctocolors"

/**
* Generates an Apple client secret and updates the environment file with the new secret.
*
* @param {Object} params
* @param {string} params.teamId - The Apple team ID.
* @param {string} params.clientId - The Apple client ID.
* @param {string} params.keyId - The Apple key ID.
* @param {string} [params.envPath=""] - The path to the environment file.
*
* @returns {Promise<void>}
*/
export async function appleGenSecret({
teamId,
clientId,
keyId,
envPath = "",
}) {
/**
* By default, the JWT has a 6 months expiry date.
*/
const expiresIn = 86400 * 180
const exp = Math.ceil(Date.now() / 1000) + expiresIn

const framework = await detectFramework(envPath)
const dotEnvFile = frameworks[framework]?.envFile
const file = join(process.cwd(), envPath, dotEnvFile)

if (!fs.existsSync(file)) {
await writeFile(file, "")
console.log(`📝 Created ${y.italic(file)}`)
}

const content = await readFile(file, "utf-8")

if (!content) {
console.error(
y.red(
`\nCould Not Find ${y.bold(
"AUTH_APPLE_PRIVATE_KEY"
)} value in ${dotEnvFile}\n`
)
)
await writeFile(
file,
"AUTH_APPLE_PRIVATE_KEY= # Must starts with `-----BEGIN PRIVATE KEY-----`,"
)
process.exit(0)
}

const keyValueMatch = content.match(/AUTH_APPLE_PRIVATE_KEY="([^"]*)"/)
const privateKey = keyValueMatch ? keyValueMatch[1].replace(/\\n/g, "\n") : ""

const clientSecret = await new SignJWT({})
.setAudience("https://appleid.apple.com")
.setIssuer(teamId)
.setIssuedAt()
.setExpirationTime(exp)
.setSubject(clientId)
.setProtectedHeader({ alg: "ES256", kid: keyId })
.sign(createPrivateKey(privateKey.replace(/\\n/g, "\n")))

await updateEnvFile({
[`AUTH_APPLE_ID`]: clientId,
[`AUTH_APPLE_SECRET`]: clientSecret,
})

console.log(
y.green(
`Apple client secret generated. Valid until: ${new Date(exp * 1000)}`
)
)
}
16 changes: 16 additions & 0 deletions lib/ensure-auth-secret-exist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { link } from "./markdown.js"
import { secret } from "../commands/index.js"
import * as y from "yoctocolors"

export async function ensureAuthSecretExist() {
console.log(
y.dim(
`\nEnsuring that ${link(
"AUTH_SECRET",
"https://authjs.dev/getting-started/installation#setup-environment"
)} is set...`
)
)

await secret.action({})
}
25 changes: 25 additions & 0 deletions lib/inquirer-prompts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { input, password } from "@inquirer/prompts"
import * as y from "yoctocolors"

/**
* @param {string} label
* @returns {Promise<string>}
*/
export async function promptInput(label) {
return input({
message: `Paste ${y.magenta(label)}:`,
validate: (value) => !!value,
})
}

/**
* @param {string} label
* @returns {Promise<string>}
*/
export async function promptPassword(label) {
return password({
message: `Paste ${y.magenta(label)}:`,
mask: true,
validate: (value) => !!value,
})
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"@inquirer/prompts": "5.1.0",
"clipboardy": "4.0.0",
"commander": "12.1.0",
"crypto": "^1.0.1",
"jose": "^5.6.3",
"open": "10.1.0",
"ora": "8.1.0",
"prompts": "2.4.2",
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

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