@rpcbase/auth 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
import isEmail from "validator/lib/isEmail"
|
|
3
|
+
|
|
4
|
+
import {Api, loadModel, Ctx, ApiHandler} from "@rpcbase/api"
|
|
5
|
+
import { hashPassword } from "@rpcbase/server"
|
|
6
|
+
|
|
7
|
+
import * as SignUp from "./index"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const signUp = async(payload: SignUp.RequestPayload, ctx: Ctx): Promise<SignUp.ResponsePayload> => {
|
|
11
|
+
const User = await loadModel("User", ctx)
|
|
12
|
+
|
|
13
|
+
const {email_or_phone, password} = SignUp.requestSchema.parse(payload)
|
|
14
|
+
|
|
15
|
+
const is_email = isEmail(email_or_phone)
|
|
16
|
+
const query = is_email ? { email: email_or_phone } : { phone: email_or_phone }
|
|
17
|
+
|
|
18
|
+
const existingUser = await User.findOne(query)
|
|
19
|
+
|
|
20
|
+
if (existingUser) {
|
|
21
|
+
console.log("user with email or phone already exists", email_or_phone)
|
|
22
|
+
// For security, we don't want to reveal if an email/phone is registered.
|
|
23
|
+
// But for this implementation, we'll make it simple.
|
|
24
|
+
return { success: false }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const salt = crypto.randomBytes(16).toString("hex")
|
|
28
|
+
const derivedKey = await hashPassword(password, salt)
|
|
29
|
+
const hashedPassword = `${salt}:${derivedKey.toString("hex")}`
|
|
30
|
+
|
|
31
|
+
const user = new User({
|
|
32
|
+
...query,
|
|
33
|
+
password: hashedPassword,
|
|
34
|
+
})
|
|
35
|
+
await user.save()
|
|
36
|
+
|
|
37
|
+
console.log("created new user", user._id.toString())
|
|
38
|
+
|
|
39
|
+
// Note: This implementation does not automatically sign the user in.
|
|
40
|
+
// A full implementation would likely create a session for the new user,
|
|
41
|
+
// but tenant assignment logic for new users is not clear at this point.
|
|
42
|
+
|
|
43
|
+
return {success: true}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default (api: Api) => {
|
|
47
|
+
api.post(SignUp.Route, signUp as ApiHandler)
|
|
48
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { isValidNumber } from "libphonenumber-js"
|
|
3
|
+
|
|
4
|
+
export const Route = "/api/rb/auth/sign-up"
|
|
5
|
+
|
|
6
|
+
export const requestSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
email_or_phone: z
|
|
9
|
+
.string()
|
|
10
|
+
.nonempty("Email or phone number is required")
|
|
11
|
+
.refine(
|
|
12
|
+
(value) => {
|
|
13
|
+
const isEmail = z.string().email().safeParse(value).success
|
|
14
|
+
const isPhone = isValidNumber(value)
|
|
15
|
+
return isEmail || isPhone
|
|
16
|
+
},
|
|
17
|
+
"Please enter a valid email address or phone number"
|
|
18
|
+
),
|
|
19
|
+
password: z.string().min(8, { message: "Password must be at least 8 characters long." }),
|
|
20
|
+
password_confirmation: z.string().nonempty({ message: "Please confirm your password." }),
|
|
21
|
+
})
|
|
22
|
+
.refine((data) => data.password === data.password_confirmation, {
|
|
23
|
+
message: "Passwords do not match.",
|
|
24
|
+
path: ["password_confirmation"],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export type RequestPayload = z.infer<typeof requestSchema>
|
|
28
|
+
|
|
29
|
+
export const responseSchema = z.object({
|
|
30
|
+
success: z.boolean(),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export type ResponsePayload = z.infer<typeof responseSchema>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import {Link, useLocation } from "@rpcbase/router"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const LINKS_REDIRECTION_MAP = {
|
|
6
|
+
"/auth/sign-in": {
|
|
7
|
+
"title": "Sign Up",
|
|
8
|
+
"location": "/auth/sign-up"
|
|
9
|
+
},
|
|
10
|
+
"/auth/sign-up": {
|
|
11
|
+
"title": "Sign In",
|
|
12
|
+
"location": "/auth/sign-in"
|
|
13
|
+
},
|
|
14
|
+
"/auth/forgot-password": {
|
|
15
|
+
"title": "Sign In",
|
|
16
|
+
"location": "/auth/sign-in"
|
|
17
|
+
},
|
|
18
|
+
"/auth/logout-success": {
|
|
19
|
+
"title": "Sign In",
|
|
20
|
+
"location": "/auth/sign-in"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Pathname = keyof typeof LINKS_REDIRECTION_MAP
|
|
25
|
+
|
|
26
|
+
export const AuthLayout = ({
|
|
27
|
+
sidePanel = null,
|
|
28
|
+
children,
|
|
29
|
+
}: {
|
|
30
|
+
sidePanel: React.ReactElement | null
|
|
31
|
+
children: React.ReactNode
|
|
32
|
+
}) => {
|
|
33
|
+
const location = useLocation()
|
|
34
|
+
|
|
35
|
+
const linkTitle = LINKS_REDIRECTION_MAP[location.pathname as Pathname]?.title
|
|
36
|
+
const linkLocation = LINKS_REDIRECTION_MAP[location.pathname as Pathname]?.location
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="container relative hidden h-dvh flex-col items-center justify-center md:grid md:w-full lg:max-w-none lg:grid-cols-2 lg:px-0">
|
|
40
|
+
{/* Top-right redirection link */}
|
|
41
|
+
<Link
|
|
42
|
+
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2 absolute right-4 top-4 md:right-8 md:top-8"
|
|
43
|
+
to={linkLocation}
|
|
44
|
+
>
|
|
45
|
+
{linkTitle}
|
|
46
|
+
</Link>
|
|
47
|
+
<div className="relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex">
|
|
48
|
+
{sidePanel}
|
|
49
|
+
</div>
|
|
50
|
+
<div className="mx-auto flex w-full flex-col justify-center gap-6 /*sm:w-[350px]*/">
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {ReactNode, useEffect} from "react"
|
|
2
|
+
import { useForm, FormProvider, zodResolver } from "@rpcbase/form"
|
|
3
|
+
|
|
4
|
+
import {requestSchema, RequestPayload} from "../../api/sign-up"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const SignUpForm = ({
|
|
8
|
+
children,
|
|
9
|
+
className
|
|
10
|
+
}: {
|
|
11
|
+
children: ReactNode,
|
|
12
|
+
className?: string
|
|
13
|
+
}) => {
|
|
14
|
+
|
|
15
|
+
const methods = useForm<RequestPayload>({
|
|
16
|
+
defaultValues: {
|
|
17
|
+
email_or_phone: "",
|
|
18
|
+
password: "",
|
|
19
|
+
password_confirmation: "",
|
|
20
|
+
},
|
|
21
|
+
resolver: zodResolver(requestSchema)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const onSubmit = async(data: RequestPayload, event) => {
|
|
25
|
+
console.log("SUBMIT SIGNUp", data)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
console.log(methods.formState.errors)
|
|
30
|
+
}, [methods.formState.errors])
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
// console.log("FOR5M", methods.formState.values)
|
|
34
|
+
}, [methods.formState.values])
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<FormProvider {...methods}>
|
|
38
|
+
<form method="post" className={className} onSubmit={methods.handleSubmit(onSubmit)}>
|
|
39
|
+
{children}
|
|
40
|
+
</form>
|
|
41
|
+
</FormProvider>
|
|
42
|
+
)
|
|
43
|
+
}
|