Skip to content

feat: add github auth #6

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
17 changes: 4 additions & 13 deletions app/api/auth/provider-callback.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
import { redirect } from 'react-router'
import { authenticator } from '~/lib/authenticator.server'
import { logger } from '~/lib/logger'
import { commitSession, getSession } from '~/lib/sessions.server'
import type { Route } from './+types/provider-callback'
import { guardSocialProvider } from '~/lib/auth/guard-social-provider'
import { commitSessionAndRedirect } from '~/lib/auth/commit-session-and-redirect'

export async function loader({ params: { provider }, request }: Route.LoaderArgs) {
if (provider !== 'google') {
throw new Response(`Unsupported provider: ${provider}`, { status: 400 })
}
guardSocialProvider(provider)
const user = await authenticator.authenticate(provider, request)
if (!user) {
logger.info({ event: 'auth_provider_login_error', message: `Login error for ${provider}` })
throw new Response('Invalid login data', { status: 400 })
}

logger.info({ event: 'auth_provider_login', message: `Login success for ${provider}`, userId: user.id })
const session = await getSession(request.headers.get('Cookie'))
session.set('user', user)

return redirect('/profile', {
headers: {
'Set-Cookie': await commitSession(session),
},
})
return commitSessionAndRedirect({ request, user })
}
6 changes: 3 additions & 3 deletions app/api/auth/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { authenticator } from '~/lib/authenticator.server'
import type { Route } from './+types/provider'

import { guardSocialProvider } from '~/lib/auth/guard-social-provider'

export const loader = async ({ params: { provider }, request }: Route.LoaderArgs) => {
if (provider !== 'google') {
throw new Response(`Unsupported provider: ${provider}`, { status: 400 })
}
guardSocialProvider(provider)
return await authenticator.authenticate(provider, request)
}
24 changes: 9 additions & 15 deletions app/features/app/route-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,6 @@ const steps = [
]

export default function RouteDashboard({ loaderData: { user } }: Route.ComponentProps) {
const active = useMemo(() => {
if (!user || !user.identities?.length) {
return 0
}
if (user.identities.length === 1) {
return 1
}
if (user.identities.find((i) => i.provider === 'Solana')) {
return 2
}
}, [user])

const done = useMemo(() => stepsDone({ user }), [user])
const nextStep = useMemo(() => {
const next = steps.find((step) => !done.includes(step.value))
Expand All @@ -111,7 +99,13 @@ export default function RouteDashboard({ loaderData: { user } }: Route.Component
radius="lg"
>
{steps.map((item) => (
<Accordion.Item key={item.value} value={item.value}>
<Accordion.Item
key={item.value}
value={item.value}
style={{
borderColor: done.includes(item.value) ? 'var(--mantine-color-green-5)' : undefined,
}}
>
<Accordion.Control color={'red'} icon={item.emoji}>
{item.label}
</Accordion.Control>
Expand Down Expand Up @@ -156,11 +150,11 @@ function stepsDone({ user }: { user: User }): string[] {
}

function findUserIdentitiesSocial({ user }: { user: User }) {
return user.identities.filter((i) => i.provider !== 'Solana')
return user.identities?.filter((i) => i.provider !== 'Solana') ?? []
}

function findUserIdentitiesSolana({ user }: { user: User }) {
return user.identities.filter((i) => i.provider === 'Solana')
return user.identities?.filter((i) => i.provider === 'Solana') ?? []
}

function PanelSocial({ user }: { user: User }) {
Expand Down
12 changes: 2 additions & 10 deletions app/features/auth/data-access/auth-handle-user-login.request.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import { redirect } from 'react-router'
import { authenticator } from '~/lib/authenticator.server'
import { logger } from '~/lib/logger'
import { commitSession, getSession } from '~/lib/sessions.server'
import { commitSessionAndRedirect } from '~/lib/auth/commit-session-and-redirect'

export async function authHandleUserLoginRequest(request: Request) {
const user = await authenticator.authenticate('user-pass', request)
if (!user) {
logger.info({ event: 'auth_login_error', message: 'User not found' })
throw new Error('Invalid login data')
}
const session = await getSession(request.headers.get('Cookie'))
session.set('user', user)

logger.info({ event: 'auth_login_success', userId: user.id })
return redirect('/profile', {
headers: {
'Set-Cookie': await commitSession(session),
},
})
return commitSessionAndRedirect({ request, user })
}
12 changes: 2 additions & 10 deletions app/features/auth/data-access/auth-handle-user-register-request.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { redirect } from 'react-router'
import { authUserRegister } from '~/features/auth/data-access/auth-user-register'
import { commitSession, getSession } from '~/lib/sessions.server'
import { logger } from '~/lib/logger'
import { commitSessionAndRedirect } from '~/lib/auth/commit-session-and-redirect'

export async function authHandleUserRegisterRequest(request: Request) {
const formData = await request.formData()
Expand All @@ -14,13 +13,6 @@ export async function authHandleUserRegisterRequest(request: Request) {
logger.info({ event: 'auth_register_error', message: 'User not registered' })
throw new Error('Invalid register data')
}
const session = await getSession(request.headers.get('Cookie'))
session.set('user', user)

logger.info({ event: 'auth_register_success', userId: user.id })
return redirect('/', {
headers: {
'Set-Cookie': await commitSession(session),
},
})
return commitSessionAndRedirect({ request, user })
}
2 changes: 1 addition & 1 deletion app/features/auth/data-access/ensure-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getUser } from '~/features/auth/data-access/get-user'
export async function ensureUser(request: Request) {
const user = await getUser(request)

if (!user) {
if (!user?.identities?.length) {
throw new Error('User not found')
}

Expand Down
11 changes: 5 additions & 6 deletions app/features/auth/data-access/get-user.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { getSession } from '~/lib/sessions.server'
import { logger } from '~/lib/logger'
import { userFindById } from '~/lib/core/user-find-by-id'

export async function getUser(request: Request) {
const session = await getSession(request.headers.get('cookie'))
import { getSessionAndUser } from '~/lib/auth/get-session-and-user'

const user = session.get('user')
if (!user) {
export async function getUser(request: Request) {
const { userId } = await getSessionAndUser(request)
if (!userId) {
return
}
const found = await userFindById(user.id)
const found = await userFindById(userId)
if (!found) {
logger.info({ event: 'auth_get_user', message: 'User not found in database' })
return
Expand Down
15 changes: 10 additions & 5 deletions app/features/auth/route-login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ export async function action({ request }: Route.ActionArgs) {
}

export async function loader({ request }: Route.LoaderArgs) {
const user = await getUser(request)
if (user) {
logger.info({ event: 'auth_login_redirect', userId: user.id, message: 'User already logged in' })
return redirect('/profile')
try {
const user = await getUser(request)
if (user) {
logger.info({ event: 'auth_login_redirect', userId: user.id, message: 'User already logged in' })
console.log(`user`, user)
return redirect('/dashboard')
}
return data(null)
} catch {
logger.info({ event: 'auth_login_redirect', message: 'User not logged in' })
}
return data(null)
}

export default function RouteLogin() {
Expand Down
19 changes: 2 additions & 17 deletions app/features/auth/route-logout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { redirect } from 'react-router'
import { appMeta } from '~/lib/app-meta'
import { logger } from '~/lib/logger'
import { destroySession, getSession } from '~/lib/sessions.server'
import type { Route } from './+types/route-login'
import { destroySessionAndRedirect } from '~/lib/auth/destroy-session-and-redirect'

export function meta() {
return appMeta('Logout')
Expand All @@ -13,18 +11,5 @@ export default function RouteLogout() {
}

export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request.headers.get('cookie'))
const user = session.get('user')

if (!user) {
logger.info({ event: 'auth_logout_error', message: 'User not found' })
return redirect('/')
}

logger.info({ event: 'auth_logout_success', userId: user.id })
return redirect('/', {
headers: {
'Set-Cookie': await destroySession(session),
},
})
return destroySessionAndRedirect({ request })
}
8 changes: 8 additions & 0 deletions app/features/auth/ui/auth-ui-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ export function AuthUiForm({ title }: { title: string }) {
<Text size="lg" fw={700} ta="center">
{title}
</Text>
<Button
component={Link}
to="/api/auth/github"
leftSection={<UiIcon name="Github" height={20} width={20} />}
size="xl"
>
Sign in with GitHub
</Button>
<Button
component={Link}
to="/api/auth/google"
Expand Down
4 changes: 2 additions & 2 deletions app/features/onboarding/onboarding-done-feature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export default function OnboardingFeature() {
</Card.Section>
<Card.Section p="sm">
<Stack style={{ overflow: 'auto' }} py="md">
<Anchor component={Link} to="/profile">
Go to Profile
<Anchor component={Link} to="/dashboard">
Go to Dashboard
</Anchor>
</Stack>
</Card.Section>
Expand Down
8 changes: 4 additions & 4 deletions app/features/profile/get-identity-url.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ export function getIdentityUrl(identity: Identity) {
case IdentityProvider.Discord:
return `https://discord.com/users/${identity.providerId}`
case IdentityProvider.Github:
return `https://github.com/${identity.name}`
return `https://github.com/${identity.address}`
case IdentityProvider.Google:
return `https://mail.google.com/mail/?view=cm&to=${identity.address}`
case IdentityProvider.Solana:
return getExplorerUrl(`address/${identity.providerId}`, 'devnet')
case IdentityProvider.Telegram:
return `https://t.me/${identity.name}`
return `https://t.me/${identity.address}`
case IdentityProvider.X:
return `https://x.com/${identity.name}`
return `https://x.com/${identity.address}`
default:
return undefined
}
}
}
2 changes: 1 addition & 1 deletion app/features/profile/profile-feature-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function ProfileFeatureDetail({ loaderData: { profile, user } }:
) : null}
</Stack>
<Flex justify="center">
<ProfileUiPoweredBy to={user ? '/dashboard' : '/'} />
<ProfileUiPoweredBy mt="xl" to={user ? '/dashboard' : '/'} />
</Flex>
</Container>
)
Expand Down
6 changes: 5 additions & 1 deletion app/features/profile/profile-feature-manage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export function meta() {
export async function loader({ request }: Route.LoaderArgs) {
try {
const user = await ensureUser(request)
if (!user?.identities?.length) {
console.log(`not user`, user)
return redirect('/login')
}
return { user, providers: Object.values(IdentityProvider) }
} catch {
return redirect('/login')
Expand Down Expand Up @@ -59,7 +63,7 @@ export default function ProfileFeatureManage({ loaderData }: Route.ComponentProp
<ProfileUiFormUpdate user={loaderData.user}></ProfileUiFormUpdate>
</UiCard>
<UiCard title="Social Identities" style={{ backgroundColor }} shadow="md" withBorder={false}>
{loaderData.user.identities.map((item) => (
{loaderData.user.identities?.map((item) => (
<div key={item.id}>
<Text size="sm" fw={500}>
{item.name}
Expand Down
6 changes: 3 additions & 3 deletions app/features/profile/profile-ui-identity-list-item.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import type { Identity } from '~/lib/generated/zod'
import { ActionIcon, Card, CopyButton, Group, Stack, Text, Tooltip } from '@mantine/core'
import { ProfileUiProviderIcon } from '~/features/profile/ui/profile-ui-provider-icon'
import { ProfileUiIdentityName } from '~/features/profile/profile-ui-identity-name'
import { UiIcon } from '~/ui/ui-icon'
import { getIdentityUrl } from '~/features/profile/get-identity-url'
import React from 'react'
import { ProfileUiProfileAvatar } from '~/features/profile/ui/profile-ui-profile-avatar'

export function ProfileUiIdentityListItem({ identity }: { identity: Identity }) {
return (
<Card withBorder bg="inherit" shadow="sm" radius="lg" p={0}>
<Group px="lg" py="md" justify="space-between" align="center">
<Group>
<ProfileUiProviderIcon provider={identity.provider} size="lg" />
<ProfileUiProfileAvatar identity={identity} />
<Stack gap={0}>
<Text size="md" fw={500}>
{identity.provider}
Expand Down Expand Up @@ -46,4 +46,4 @@ export function ProfileUiIdentityListItem({ identity }: { identity: Identity })
</Group>
</Card>
)
}
}
8 changes: 4 additions & 4 deletions app/features/profile/profile-ui-powered-by.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Button, Text } from '@mantine/core'
import { Button, type ButtonProps, Text } from '@mantine/core'
import { UiLogo } from '~/ui/ui-logo'
import React from 'react'
import { Link } from 'react-router'

export function ProfileUiPoweredBy({ to }: { to: string }) {
export function ProfileUiPoweredBy({ to, ...props }: ButtonProps & { to: string }) {
return (
<Button component={Link} to={to} radius="xl" variant="outline" color="dark" c="dimmed">
<Text span mr="xs" fz="inherit">
<Button component={Link} to={to} radius="xl" variant="outline" color="dark" c="dimmed" {...props}>
<Text span mr="sm" fz="inherit">
Powered by
</Text>
<UiLogo height={24} width={96} />
Expand Down
19 changes: 19 additions & 0 deletions app/features/profile/ui/profile-ui-profile-avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Avatar, type AvatarProps } from '@mantine/core'
import type { UiIconProps } from '~/ui/ui-icon'
import type { Identity } from '~/lib/generated/zod'
import { ProfileUiProviderIcon } from '~/features/profile/ui/profile-ui-provider-icon'

export function ProfileUiProfileAvatar({
identity,
iconProps,
...props
}: AvatarProps & {
iconProps?: Omit<UiIconProps, 'name'>
identity: Identity
}) {
const avatarUrl = (identity?.profile as Record<string, string>)?.avatarUrl
if (avatarUrl) {
return <Avatar src={avatarUrl} {...props} />
}
return <ProfileUiProviderIcon provider={identity.provider} {...iconProps} />
}
19 changes: 19 additions & 0 deletions app/features/solana/ensure-verified-payload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { SolanaAuthMessageSigned } from '~/lib/solana-auth/solana-auth-message'
import { solanaAuth } from '~/lib/solana-auth/solana-auth'

function parsePayload(payload: string = ''): SolanaAuthMessageSigned {
try {
return JSON.parse(payload)
} catch {
throw new Error(`Invalid payload`)
}
}

export async function ensureVerifiedPayload(payload: string = '') {
const parsed = parsePayload(payload)
const result = await solanaAuth.verifyMessage(parsed)
if (!result) {
throw new Error('Invalid signature')
}
return result
}
Loading