Skip to content

Commit 74e0d98

Browse files
committed
update website
1 parent 9fca83d commit 74e0d98

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+9083
-470
lines changed

.gitignore copy

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# dependencies
2+
node_modules
3+
.yarn/*
4+
!.yarn/patches
5+
!.yarn/plugins
6+
!.yarn/releases
7+
!.yarn/sdks
8+
!.yarn/versions
9+
.pnp.*
10+
.npm
11+
web_modules/
12+
13+
# blitz
14+
/.blitz/
15+
/.next/
16+
*.sqlite
17+
*.sqlite-journal
18+
.now
19+
.blitz**
20+
blitz-log.log
21+
22+
# misc
23+
.DS_Store
24+
25+
# local env files
26+
.env.local
27+
.env.*.local
28+
.envrc
29+
30+
# Logs
31+
logs
32+
*.log
33+
34+
# Runtime data
35+
pids
36+
*.pid
37+
*.seed
38+
*.pid.lock
39+
40+
# Testing
41+
.coverage
42+
*.lcov
43+
.nyc_output
44+
lib-cov
45+
46+
# Caches
47+
*.tsbuildinfo
48+
.eslintcache
49+
.node_repl_history
50+
.yarn-integrity
51+
52+
# Serverless directories
53+
.serverless/
54+
55+
# Stores VSCode versions used for testing VSCode extensions
56+
.vscode-test

.npmrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
save-exact=true
2+
legacy-peer-deps=true
3+
4+
public-hoist-pattern[]=@tanstack/react-query
5+
public-hoist-pattern[]=next
6+
public-hoist-pattern[]=secure-password
7+
public-hoist-pattern[]=*jest*
8+
public-hoist-pattern[]=@testing-library/*

.prettierignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.gitkeep
2+
.env*
3+
*.ico
4+
*.lock
5+
db/migrations
6+
.next
7+
.yarn
8+
.pnp.*
9+
node_modules

app/auth/components/LoginForm.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { AuthenticationError, PromiseReturnType } from "blitz"
2+
import Link from "next/link"
3+
import { LabeledTextField } from "app/core/components/LabeledTextField"
4+
import { Form, FORM_ERROR } from "app/core/components/Form"
5+
import login from "app/auth/mutations/login"
6+
import { Login } from "app/auth/validations"
7+
import { useMutation } from "@blitzjs/rpc"
8+
import { Routes } from "@blitzjs/next"
9+
10+
type LoginFormProps = {
11+
onSuccess?: (user: PromiseReturnType<typeof login>) => void
12+
}
13+
14+
export const LoginForm = (props: LoginFormProps) => {
15+
const [loginMutation] = useMutation(login)
16+
return (
17+
<div className="flex flex-col items-center justify-center" style={{height: "100vh"}}>
18+
<h1>Login</h1>
19+
20+
<Form
21+
submitText="Login"
22+
schema={Login}
23+
initialValues={{ email: "", password: "" }}
24+
onSubmit={async (values) => {
25+
try {
26+
const user = await loginMutation(values)
27+
props.onSuccess?.(user)
28+
} catch (error: any) {
29+
if (error instanceof AuthenticationError) {
30+
return { [FORM_ERROR]: "Sorry, those credentials are invalid" }
31+
} else {
32+
return {
33+
[FORM_ERROR]:
34+
"Sorry, we had an unexpected error. Please try again. - " + error.toString(),
35+
}
36+
}
37+
}
38+
}}
39+
>
40+
<LabeledTextField name="email" label="Email" placeholder="Email" />
41+
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
42+
<div>
43+
<Link href={Routes.ForgotPasswordPage()} passHref>
44+
<a>Forgot your password?</a>
45+
</Link>
46+
</div>
47+
</Form>
48+
49+
<div style={{ marginTop: "1rem" }}>
50+
Or{" "}
51+
<Link href={Routes.SignupPage()} passHref>
52+
<a>Sign Up</a>
53+
</Link>
54+
</div>
55+
</div>
56+
)
57+
}
58+
59+
export default LoginForm

app/auth/components/SignupForm.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { LabeledTextField } from "app/core/components/LabeledTextField"
2+
import { Form, FORM_ERROR } from "app/core/components/Form"
3+
import signup from "app/auth/mutations/signup"
4+
import { Signup } from "app/auth/validations"
5+
import { useMutation } from "@blitzjs/rpc"
6+
7+
type SignupFormProps = {
8+
onSuccess?: () => void
9+
}
10+
11+
export const SignupForm = (props: SignupFormProps) => {
12+
const [signupMutation] = useMutation(signup)
13+
return (
14+
<div className="flex flex-col items-center justify-center" style={{height: "100vh"}}>
15+
<h1>Create an Account</h1>
16+
17+
<Form
18+
submitText="Create Account"
19+
schema={Signup}
20+
initialValues={{ email: "", password: "" }}
21+
onSubmit={async (values) => {
22+
try {
23+
await signupMutation(values)
24+
props.onSuccess?.()
25+
} catch (error: any) {
26+
if (error.code === "P2002" && error.meta?.target?.includes("email")) {
27+
// This error comes from Prisma
28+
return { email: "This email is already being used" }
29+
} else {
30+
return { [FORM_ERROR]: error.toString() }
31+
}
32+
}
33+
}}
34+
>
35+
<LabeledTextField name="email" label="Email" placeholder="Email" />
36+
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
37+
</Form>
38+
</div>
39+
)
40+
}
41+
42+
export default SignupForm

app/auth/mutations/changePassword.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NotFoundError } from "blitz"
2+
import db from "db"
3+
import { authenticateUser } from "./login"
4+
import { ChangePassword } from "../validations"
5+
import { resolver } from "@blitzjs/rpc"
6+
import { SecurePassword } from "@blitzjs/auth"
7+
8+
export default resolver.pipe(
9+
resolver.zod(ChangePassword),
10+
resolver.authorize(),
11+
async ({ currentPassword, newPassword }, ctx) => {
12+
const user = await db.user.findFirst({ where: { id: ctx.session.userId as string } })
13+
if (!user) throw new NotFoundError()
14+
15+
await authenticateUser(user.email, currentPassword)
16+
17+
const hashedPassword = await SecurePassword.hash(newPassword.trim())
18+
await db.user.update({
19+
where: { id: user.id },
20+
data: { hashedPassword },
21+
})
22+
23+
return true
24+
}
25+
)

app/auth/mutations/forgotPassword.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { generateToken, hash256 } from "@blitzjs/auth"
2+
import { resolver } from "@blitzjs/rpc"
3+
import db from "db"
4+
import { forgotPasswordMailer } from "mailers/forgotPasswordMailer"
5+
import { ForgotPassword } from "../validations"
6+
7+
const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4
8+
9+
export default resolver.pipe(resolver.zod(ForgotPassword), async ({ email }) => {
10+
// 1. Get the user
11+
const user = await db.user.findFirst({ where: { email: email.toLowerCase() } })
12+
13+
// 2. Generate the token and expiration date.
14+
const token = generateToken()
15+
const hashedToken = hash256(token)
16+
const expiresAt = new Date()
17+
expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS)
18+
19+
// 3. If user with this email was found
20+
if (user) {
21+
// 4. Delete any existing password reset tokens
22+
await db.token.deleteMany({ where: { type: "RESET_PASSWORD", userId: user.id } })
23+
// 5. Save this new token in the database.
24+
await db.token.create({
25+
data: {
26+
user: { connect: { id: user.id } },
27+
type: "RESET_PASSWORD",
28+
expiresAt,
29+
hashedToken,
30+
sentTo: user.email,
31+
},
32+
})
33+
// 6. Send the email
34+
await forgotPasswordMailer({ to: user.email, token }).send()
35+
} else {
36+
// 7. If no user found wait the same time so attackers can't tell the difference
37+
await new Promise((resolve) => setTimeout(resolve, 750))
38+
}
39+
40+
// 8. Return the same result whether a password reset email was sent or not
41+
return
42+
})

app/auth/mutations/login.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { SecurePassword } from "@blitzjs/auth"
2+
import { resolver } from "@blitzjs/rpc"
3+
import { AuthenticationError } from "blitz"
4+
import db from "db"
5+
import { Role } from "types"
6+
import { Login } from "../validations"
7+
8+
export const authenticateUser = async (rawEmail: string, rawPassword: string) => {
9+
const { email, password } = Login.parse({ email: rawEmail, password: rawPassword })
10+
const user = await db.user.findFirst({ where: { email } })
11+
if (!user) throw new AuthenticationError()
12+
13+
const result = await SecurePassword.verify(user.hashedPassword, password)
14+
15+
if (result === SecurePassword.VALID_NEEDS_REHASH) {
16+
// Upgrade hashed password with a more secure hash
17+
const improvedHash = await SecurePassword.hash(password)
18+
await db.user.update({ where: { id: user.id }, data: { hashedPassword: improvedHash } })
19+
}
20+
21+
const { hashedPassword, ...rest } = user
22+
return rest
23+
}
24+
25+
export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ctx) => {
26+
// This throws an error if credentials are invalid
27+
const user = await authenticateUser(email, password)
28+
29+
await ctx.session.$create({ userId: user.id, role: user.role as Role })
30+
31+
return user
32+
})

app/auth/mutations/logout.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Ctx } from "blitz"
2+
3+
export default async function logout(_: any, ctx: Ctx) {
4+
return await ctx.session.$revoke()
5+
}

app/auth/mutations/resetPassword.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { SecurePassword, hash256 } from "@blitzjs/auth"
2+
import db from "db"
3+
import { ResetPassword } from "../validations"
4+
import login from "./login"
5+
6+
export class ResetPasswordError extends Error {
7+
name = "ResetPasswordError"
8+
message = "Reset password link is invalid or it has expired."
9+
}
10+
11+
export default async function resetPassword(input, ctx) {
12+
ResetPassword.parse(input)
13+
// 1. Try to find this token in the database
14+
const hashedToken = hash256(input.token)
15+
const possibleToken = await db.token.findFirst({
16+
where: { hashedToken, type: "RESET_PASSWORD" },
17+
include: { user: true },
18+
})
19+
20+
// 2. If token not found, error
21+
if (!possibleToken) {
22+
throw new ResetPasswordError()
23+
}
24+
const savedToken = possibleToken
25+
26+
// 3. Delete token so it can't be used again
27+
await db.token.delete({ where: { id: savedToken.id } })
28+
29+
// 4. If token has expired, error
30+
if (savedToken.expiresAt < new Date()) {
31+
throw new ResetPasswordError()
32+
}
33+
34+
// 5. Since token is valid, now we can update the user's password
35+
const hashedPassword = await SecurePassword.hash(input.password.trim())
36+
const user = await db.user.update({
37+
where: { id: savedToken.userId },
38+
data: { hashedPassword },
39+
})
40+
41+
// 6. Revoke all existing login sessions for this user
42+
await db.session.deleteMany({ where: { userId: user.id } })
43+
44+
// 7. Now log the user in with the new credentials
45+
await login({ email: user.email, password: input.password }, ctx)
46+
47+
return true
48+
}

app/auth/mutations/signup.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import db from "db"
2+
import { SecurePassword } from "@blitzjs/auth"
3+
import { Role } from "types"
4+
5+
export default async function signup(input, ctx) {
6+
const blitzContext = ctx
7+
8+
const hashedPassword = await SecurePassword.hash((input.password as string) || "test-password")
9+
const email = (input.email as string) || "test" + Math.random() + "@test.com"
10+
const user = await db.user.create({
11+
data: { email, hashedPassword, role: "user" },
12+
select: { id: true, name: true, email: true, role: true },
13+
})
14+
15+
await blitzContext.session.$create({
16+
userId: user.id,
17+
role: user.role as Role,
18+
})
19+
20+
return { userId: blitzContext.session.userId, ...user, email: input.email }
21+
}

0 commit comments

Comments
 (0)