diff --git a/package-lock.json b/package-lock.json
index c32293f9..3ec07985 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
+ "@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@mui/icons-material": "^5.16.5",
@@ -963,6 +964,17 @@
"node": ">=6"
}
},
+ "node_modules/@fortawesome/free-brands-svg-icons": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
+ "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
diff --git a/package.json b/package.json
index bea2a467..24eea081 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
+ "@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@mui/icons-material": "^5.16.5",
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
index 1d38a1d9..85cc4d25 100644
--- a/src/app/api/auth/[...nextauth]/route.ts
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -1,3 +1,3 @@
-import { auth } from "@/composition"
+import { authHandlers } from "@/composition"
-export const { GET, POST } = auth.handlers
+export const { GET, POST } = authHandlers
diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx
new file mode 100644
index 00000000..97e53ae0
--- /dev/null
+++ b/src/app/auth/signin/page.tsx
@@ -0,0 +1,127 @@
+import Image from "next/image"
+import Link from "next/link"
+import { Box, Button, Stack, Typography } from "@mui/material"
+import { signIn } from "@/composition"
+import { env } from "@/common"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import { faGithub } from "@fortawesome/free-brands-svg-icons"
+import SignInTexts from "@/features/auth/view/SignInTexts"
+
+const SITE_NAME = env.getOrThrow("NEXT_PUBLIC_SHAPE_DOCS_TITLE")
+const HELP_URL = env.get("NEXT_PUBLIC_SHAPE_DOCS_HELP_URL")
+
+export default async function SignInPage() {
+ return (
+
+
+
+
+ )
+}
+
+const InfoColumn = () => {
+ return (
+
+
+
+ )
+}
+
+const SignInColumn = () => {
+ const title = `Get started with ${SITE_NAME}`
+ return (
+
+
+
+
+
+
+
+ {title}
+
+
+ {title}
+
+
+
+
+
+
+
+
+ )
+}
+
+const SignInWithGitHub = () => {
+ return (
+
+ )
+}
+
+const Footer = () => {
+ return (
+
+ {HELP_URL &&
+
+
+ Learn more about {SITE_NAME}
+
+
+ }
+
+ )
+}
\ No newline at end of file
diff --git a/src/common/session/AuthjsSession.ts b/src/common/session/AuthjsSession.ts
index 5d3ff360..5b3686ae 100644
--- a/src/common/session/AuthjsSession.ts
+++ b/src/common/session/AuthjsSession.ts
@@ -1,23 +1,21 @@
-import { NextAuthResult } from "next-auth"
+import { Session } from "next-auth"
import { UnauthorizedError } from "@/common"
import ISession from "./ISession"
export default class AuthjsSession implements ISession {
- private readonly auth: NextAuthResult
+ private readonly auth: () => Promise
- constructor(config: { auth: NextAuthResult }) {
+ constructor(config: { auth: () => Promise }) {
this.auth = config.auth
}
async getIsAuthenticated(): Promise {
- const { auth } = this.auth
- const session = await auth()
+ const session = await this.auth()
return session != null
}
async getUserId(): Promise {
- const { auth } = this.auth
- const session = await auth()
+ const session = await this.auth()
if (!session || !session.user || !session.user.id) {
throw new UnauthorizedError("User ID is unavailable because the user is not authenticated.")
}
diff --git a/src/composition.ts b/src/composition.ts
index 88c7316c..7284f036 100644
--- a/src/composition.ts
+++ b/src/composition.ts
@@ -79,7 +79,7 @@ const oauthTokenRepository = new FallbackOAuthTokenRepository({
const logInHandler = new LogInHandler({ oauthTokenRepository })
-export const auth = NextAuth({
+export const { signIn, auth, handlers: authHandlers } = NextAuth({
adapter: PostgresAdapter(pool),
secret: env.getOrThrow("NEXTAUTH_SECRET"),
theme: {
@@ -87,6 +87,9 @@ export const auth = NextAuth({
colorScheme: "light",
brandColor: "black"
},
+ pages: {
+ signIn: "/auth/signin"
+ },
providers: [
GithubProvider({
clientId: env.getOrThrow("GITHUB_CLIENT_ID"),
diff --git a/src/features/auth/view/SignInTexts.tsx b/src/features/auth/view/SignInTexts.tsx
new file mode 100644
index 00000000..602ce70f
--- /dev/null
+++ b/src/features/auth/view/SignInTexts.tsx
@@ -0,0 +1,92 @@
+"use client"
+
+import { Box, Typography, SxProps } from "@mui/material"
+import { useEffect, useState, useMemo } from "react"
+
+const SignInTexts = () => {
+ const getRandomTextColor = ({ excluding }: { excluding?: string }) => {
+ const colors = ["#01BBFE", "#00AE47", "#FCB23D"]
+ .filter(e => e !== excluding)
+ return colors[Math.floor(Math.random() * colors.length)]
+ }
+ const [characterIndex, setCharacterIndex] = useState(0)
+ const [textIndex, setTextIndex] = useState(0)
+ const [displayedText, setDisplayedText] = useState("")
+ const [textColor, setTextColor] = useState(getRandomTextColor({}))
+ const texts = useMemo(() => [
+ "is a great OpenAPI viewer",
+ "facilitates spec-driven development",
+ "puts your documentation in one place",
+ "adds documentation previews to pull requests"
+ ], [])
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setDisplayedText("")
+ setCharacterIndex(0)
+ setTextColor(getRandomTextColor({ excluding: textColor }))
+ setTextIndex((prevIndex) => (prevIndex + 1) % texts.length)
+ }, 5000)
+ return () => clearInterval(interval)
+ }, [texts.length, textColor])
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCharacterIndex(characterIndex + 1)
+ setDisplayedText(texts[textIndex].substring(0, characterIndex))
+ if (characterIndex === texts[textIndex].length) {
+ clearInterval(interval)
+ }
+ }, 50)
+ return () => clearInterval(interval)
+ }, [texts, textIndex, characterIndex])
+ const longestText = texts.reduce((a, b) => (a.length > b.length ? a : b))
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default SignInTexts
+
+const Text = ({
+ text,
+ textColor,
+ children,
+ sx
+}: {
+ text: string,
+ textColor?: string,
+ children?: React.ReactNode,
+ sx?: SxProps
+}) => {
+ return (
+
+ Shape Docs {text}
+ {children}
+
+ )
+}
diff --git a/wiki/login.png b/wiki/login.png
index c41680bf..fa471489 100644
Binary files a/wiki/login.png and b/wiki/login.png differ