Implement SignIn/SignOut

This commit is contained in:
Jorge Vargas 2024-11-15 12:58:43 -06:00
parent bce35d73ca
commit adeb3fd3bf
16 changed files with 987 additions and 77 deletions

View file

@ -2,6 +2,7 @@
import { defineConfig } from 'astro/config'
import tailwind from '@astrojs/tailwind'
import node from '@astrojs/node'
import react from '@astrojs/react';
import paraglide from '@inlang/paraglide-astro'
import auth from 'auth-astro'
import icon from 'astro-icon'
@ -23,8 +24,9 @@ export default defineConfig({
tailwind(),
auth(),
paraglide({ project: './project.inlang', outdir: './src/paraglide' }),
icon({ iconDir: 'src/img/icons' })
icon({ iconDir: 'src/img/icons' }),
react()
],
output: 'server',
adapter: node({ mode: 'standalone' })
})
})

View file

@ -18,7 +18,10 @@ export default defineConfig({
async authorize(credentials) {
if (!credentials?.username || !credentials.password) throw new InvalidLoginError()
const user = await prismaClient.users.findUnique({ where: { username: credentials.username } })
const user = await prismaClient.users.findUnique({
select: { username: true, password: true },
where: { username: credentials.username }
})
if (!user) throw new InvalidLoginError()
const valid = await bcrypt.compare(credentials.password, user.password)

View file

@ -3,6 +3,10 @@
"register": "Register",
"login": "Login",
"logout": "Logout",
"username": "Username",
"password": "Password",
"email": "Email",
"recoverPassword": "Recover Password",
"home": "Home",
"lastaddednav": "Last Added",
"albumlist": "Album List",
@ -21,5 +25,6 @@
"managealbums": "Manage Albums",
"manageusers": "Manage Users",
"managerequests": "Manage Requests",
"managesubmissions": "Manage Submissions"
"managesubmissions": "Manage Submissions",
"profilePic": "Profile picture"
}

View file

@ -11,7 +11,9 @@
},
"dependencies": {
"@apollo/client": "^3.11.4",
"@apollo/server": "^4.11.2",
"@astrojs/node": "^8.3.3",
"@astrojs/react": "^3.6.2",
"@astrojs/rss": "^4.0.7",
"@astrojs/tailwind": "^5.1.1",
"@auth/core": "^0.35.0",
@ -22,13 +24,24 @@
"@graphql-tools/schema": "^10.0.4",
"@inlang/paraglide-astro": "^0.2.2",
"@prisma/client": "^5.22.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"apollo-upload-client": "^18.0.1",
"astro": "^4.15.7",
"astro-icon": "^1.1.1",
"auth-astro": "^4.1.2",
"bcrypt": "^5.1.1",
"clsx": "^2.1.1",
"fs-extra": "^11.2.0",
"generate-password-ts": "^1.6.5",
"graphql": "^16.9.0",
"graphql-scalars": "^1.23.0",
"graphql-yoga": "^5.10.2",
"nodemailer": "^6.9.16",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-svg-spinners": "^0.3.1",
"sharp": "^0.33.5",
"tailwindcss": "^3.4.12",
"typescript": "^5.6.2"
},

View file

@ -317,10 +317,10 @@ model favorites {
model forgors {
id Int @id @default(autoincrement())
expires DateTime? @db.DateTime(0)
expires DateTime? @default(dbgenerated("DATE_ADD(NOW(), INTERVAL 24 HOUR)")) @db.DateTime(0)
key String? @db.VarChar(255)
createdAt DateTime @db.DateTime(0)
updatedAt DateTime @db.DateTime(0)
createdAt DateTime @default(now()) @db.DateTime(0)
updatedAt DateTime @default(now()) @db.DateTime(0)
username String? @db.VarChar(255)
users users? @relation(fields: [username], references: [username], map: "forgors_ibfk_1")
@ -515,8 +515,8 @@ model users {
username String @id @db.VarChar(255)
email String? @db.VarChar(255)
password String? @db.VarChar(255)
createdAt DateTime @db.DateTime(0)
updatedAt DateTime @db.DateTime(0)
createdAt DateTime @default(now()) @db.DateTime(0)
updatedAt DateTime @default(now()) @db.DateTime(0)
placeholder String? @db.Text
imgId String? @db.VarChar(255)
User_Role User_Role[]

View file

@ -1,12 +0,0 @@
---
const { class: className } = Astro.props
---
<button
class:list={[
'bg-blue-600 hover:bg-blue-700 py-2 px-3.5 rounded-lg',
className
]}
>
<slot />
</button>

26
src/components/Button.tsx Normal file
View file

@ -0,0 +1,26 @@
import type { PropsWithChildren } from 'react'
import clsx from 'clsx'
import { BarsRotateFade } from 'react-svg-spinners'
export default function Button(props: PropsWithChildren<{ className?: string; loading?: boolean }>) {
const { children, className, loading = false, ...restProps } = props
return (
<button
className={clsx(
loading ? 'bg-blue-400 cursor-progress' : 'bg-blue-600 hover:bg-blue-700',
'py-2 px-3.5 rounded-lg',
className
)}
{...restProps}
>
<div className='relative flex'>
<span className={clsx({ invisible: loading })}>{children}</span>
{loading ? (
<div className='absolute top-0 left-0 w-full flex justify-center'>
<BarsRotateFade color='white' />
</div>
) : null}
</div>
</button>
)
}

View file

@ -1,6 +1,6 @@
---
import { gql } from '@/graphql/__generated__/client/index.js'
import { getApolloClient } from '@/graphql/apolloClient.js'
import { getApolloClient } from '@/graphql/apolloClientSSR.js'
import { Image, Picture } from 'astro:assets'
import { getSession } from 'auth-astro/server'
import * as m from '../paraglide/messages.js'
@ -8,11 +8,11 @@ import * as m from '../paraglide/messages.js'
import logo from 'img/logos/winter.png'
// import logoEs from 'img/logos/default_es.png'
import Button from './Button.astro'
import Dropdown from './header/Dropdown.astro'
import DropdownItem from './header/DropdownItem.astro'
import Toggler from './header/Toggler.astro'
import NavButton from './header/NavButton.astro'
import LoginNav from './header/LoginNav.astro'
const headerQuery = gql(`
query HeaderInfo {
@ -48,16 +48,7 @@ const session = await getSession(Astro.request)
<Image src={logo} class='h-full py-0.5 w-auto' alt='logo' height={150} width={265} />
</a>
<div class='absolute top-0 right-0 space-x-2 mr-10'>
{
session === null ? (
<Button class='rounded-t-none'>{m.login()}</Button>
) : (
<Button class='rounded-t-none'>{m.logout()}</Button>
)
}
<Button class='rounded-t-none'>{m.register()}</Button>
</div>
<LoginNav />
</div>
</div>
<nav class='w-full md:h-[55px] bg-dark'>

View file

@ -0,0 +1,15 @@
---
import { getSession } from 'auth-astro/server'
import { SignIn, SignOut } from 'auth-astro/components'
import RegisterBtn from './RegisterButton'
import * as m from 'paraglide/messages.js'
const session = await getSession(Astro.request)
const btnClass = 'bg-blue-600 hover:bg-blue-700 py-2 px-3.5 rounded-lg rounded-t-none'
---
<div class='absolute top-0 right-0 space-x-2 mr-10'>
{session === null ? <SignIn class={btnClass}>{m.login()}</SignIn> : <SignOut class={btnClass}>{m.logout()}</SignOut>}
{!session ? <RegisterBtn client:only='react' /> : null}
</div>

View file

@ -1,14 +1,15 @@
---
import Button from '../Button.astro'
import Button from '../Button'
import clsx from 'clsx'
const { class: className } = Astro.props
---
<Button
class:list={[
className={clsx(
'w-full md:w-fit md:h-full bg-dark hover:bg-dark-hover py-3.5 md:py-1 px-2 rounded-none text-left md:text-center',
className
]}
)}
>
<slot />
</Button>

View file

@ -1,23 +1,15 @@
import { ApolloClient, InMemoryCache } from '@apollo/client/core'
import { SchemaLink } from '@apollo/client/link/schema'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { getSession } from 'auth-astro/server'
import type { Session } from '@auth/core/types'
import prismaClient, { type users } from 'prisma/client'
import createUploadLink from 'apollo-upload-client/createUploadLink.mjs'
import { typeDefs } from './__generated__/typeDefs.generated'
import resolvers from '@/graphql/resolvers'
const httpLink = createUploadLink({
uri: `${window.origin}/api/graphql`,
headers: { 'Apollo-Require-Preflight': true },
credentials: 'include'
})
const schema = makeExecutableSchema({ typeDefs, resolvers })
export type ResolverContext = { request?: Request; session?: Session; user?: users }
const apolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
})
export async function getApolloClient(request?: Request) {
const session = async () => (request ? getSession(request) : null)
const user = async () => (session ? prismaClient.users.findUnique({ where: { username: session.id } }) : null)
return new ApolloClient({
ssrMode: true,
link: new SchemaLink({ schema, context: { request, session, user } }),
cache: new InMemoryCache()
})
}
export default apolloClient

View file

@ -0,0 +1,24 @@
import { ApolloClient, InMemoryCache } from '@apollo/client/core'
import { SchemaLink } from '@apollo/client/link/schema'
import { getSession } from 'auth-astro/server'
import type { Session } from '@auth/core/types'
import prismaClient, { type users } from 'prisma/client'
import { makeExecutableSchema } from '@graphql-tools/schema'
import { typeDefs } from '@/graphql/__generated__/typeDefs.generated'
import resolvers from '@/graphql/resolvers'
export type ResolverContext = { request?: Request; session?: Session; user?: users }
const schema = makeExecutableSchema({ typeDefs, resolvers })
export async function getApolloClient(request?: Request) {
const session = async () => (request ? getSession(request) : null)
const user = async () => (session ? prismaClient.users.findUnique({ where: { username: session.id } }) : null)
return new ApolloClient({
ssrMode: true,
link: new SchemaLink({ schema, context: { request, session, user } }),
cache: new InMemoryCache()
})
}

View file

@ -38,10 +38,7 @@ type Query {
}
type Mutation {
login(username: String!, password: String!): Int!
logout: Int!
registerUser(email: String!, username: String!, pfp: Upload): Boolean!
registerUser(email: String!, username: String!, pfp: File): Boolean!
updateUserRoles(username: String!, roles: [String]!): Boolean!
deleteUser(username: String!): Int

View file

@ -1,7 +1,7 @@
import rss, { type RSSFeedItem } from '@astrojs/rss'
import type { APIContext } from 'astro'
import { getApolloClient } from '@/graphql/apolloClient.js'
import { getApolloClient } from '@/graphql/apolloClientSSR'
import { gql } from '@/graphql/__generated__/client'
const addedQuery = gql(`

View file

@ -0,0 +1,13 @@
import { GraphQLError } from 'graphql'
import { ApolloServerErrorCode } from '@apollo/server/errors'
export const AuthenticationError = (message: string = '') =>
new GraphQLError(message, { extensions: { code: 'UNAUTHENTICATED' } })
export const ForbiddenError = (message: string = '') =>
new GraphQLError(message, { extensions: { code: 'FORBIDDEN' } })
export const UserInputError = (message: string = '') =>
new GraphQLError(message, {
extensions: { code: ApolloServerErrorCode.BAD_USER_INPUT }
})

874
yarn.lock

File diff suppressed because it is too large Load diff