Skip to content

Commit e890971

Browse files
committed
Merge remote-tracking branch 'origin/main' into yu/breaking/collection
2 parents f93c892 + 76ce28f commit e890971

12 files changed

Lines changed: 79 additions & 28 deletions

File tree

.changeset/shy-rice-grow.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@example/erp": patch
3+
"@kivotos/core": minor
4+
"@kivotos/next": minor
5+
---
6+
7+
[[DRIZZ-31] Register Page](https://app.plane.so/softnetics/browse/DRIZZ-31/)
8+
[[DRIZZ-32] Login page](https://app.plane.so/softnetics/browse/DRIZZ-32/)

packages/core/src/auth/handlers/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import type { AuthConfig } from '..'
1010
export function createAuthHandlers<TAuthConfig extends AuthConfig>(config: TAuthConfig) {
1111
const handlers = {
1212
// No authentication required
13-
signUp: signUp({}),
14-
loginEmail: loginEmail({}),
13+
signUp: signUp(config),
14+
loginEmail: loginEmail(config),
1515
signOut: signOut({}),
1616
resetPasswordEmail: resetPasswordEmail({}),
1717
forgotPasswordEmail: forgotPasswordEmail({}),

packages/core/src/auth/handlers/login-email.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import z from 'zod'
22

33
import { type ApiRouteHandler, type ApiRouteSchema, createEndpoint } from '../../endpoint'
4+
import type { AuthConfig } from '..'
45
import { AccountProvider } from '../constant'
56
import { type AuthContext } from '../context'
6-
import { setSessionCookie } from '../utils'
7+
import { setSessionCookie, verifyPassword } from '../utils'
78

8-
interface InternalRouteOptions {
9-
prefix?: string
10-
}
11-
12-
export function loginEmail<const TOptions extends InternalRouteOptions>(options: TOptions) {
9+
export function loginEmail<const TOptions extends AuthConfig>(options: TOptions) {
1310
const schema = {
1411
method: 'POST',
1512
path: '/api/auth/login-email',
@@ -36,9 +33,8 @@ export function loginEmail<const TOptions extends InternalRouteOptions>(options:
3633
AccountProvider.CREDENTIAL
3734
)
3835

39-
// TODO: Hash password and compare
40-
const hashPassword = account.password
41-
if (account.password !== hashPassword) {
36+
const verifyStatus = await verifyPassword(args.body.password, account.password as string)
37+
if (!verifyStatus) {
4238
throw new Error('Invalid password')
4339
}
4440

packages/core/src/auth/handlers/sign-up.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import z from 'zod'
22

33
import { type ApiRouteHandler, type ApiRouteSchema, createEndpoint } from '../../endpoint'
4+
import type { AuthConfig } from '..'
45
import { AccountProvider } from '../constant'
56
import { type AuthContext } from '../context'
7+
import { hashPassword, setSessionCookie } from '../utils'
68

7-
interface InternalRouteOptions {}
8-
9-
export function signUp<const TOptions extends InternalRouteOptions>(options: TOptions) {
9+
export function signUp<const TOptions extends AuthConfig>(options: TOptions) {
1010
const schema = {
1111
method: 'POST',
1212
path: '/api/auth/sign-up',
@@ -31,7 +31,9 @@ export function signUp<const TOptions extends InternalRouteOptions>(options: TOp
3131
} as const satisfies ApiRouteSchema
3232

3333
const handler: ApiRouteHandler<AuthContext, typeof schema> = async (args) => {
34-
const hasedPassword = args.body.password // TODO: hash password
34+
const hashedPassword =
35+
(await options.emailAndPassword?.passwordHasher?.(args.body.password)) ??
36+
(await hashPassword(args.body.password))
3537

3638
const user = await args.context.internalHandlers.user.create({
3739
name: args.body.name,
@@ -43,7 +45,7 @@ export function signUp<const TOptions extends InternalRouteOptions>(options: TOp
4345
userId: user.id,
4446
providerId: AccountProvider.CREDENTIAL,
4547
accountId: user.id,
46-
password: hasedPassword,
48+
password: hashedPassword,
4749
})
4850

4951
// TODO: Check if sending email verification is enabled
@@ -56,15 +58,19 @@ export function signUp<const TOptions extends InternalRouteOptions>(options: TOp
5658
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
5759
})
5860

61+
const responseHeaders = {}
62+
if (options.emailAndPassword?.signUp?.autoLogin !== false) {
63+
// Set session cookie if auto login is enabled
64+
setSessionCookie(responseHeaders, session.token)
65+
}
66+
5967
return {
6068
status: 200,
6169
body: {
6270
token: session.token,
6371
user: user,
6472
},
65-
headers: {
66-
'Set-Cookie': `kivotosSession=${session.token}; Path=/; HttpOnly; Secure; SameSite=Strict`,
67-
},
73+
headers: responseHeaders,
6874
}
6975
}
7076

packages/core/src/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export interface AuthConfig {
6464
}
6565
emailAndPassword?: {
6666
enabled: boolean
67+
passwordHasher?: (password: string) => Promise<string> // default: scrypt
6768
signUp?: {
6869
autoLogin?: boolean // default: true
6970
additionalFields?: Fields<any>

packages/core/src/auth/utils.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
import { parse as parseCookies } from 'cookie-es'
1+
import { parse as parseCookies, serialize } from 'cookie-es'
2+
import crypto, { randomBytes } from 'crypto'
3+
import { promisify } from 'util'
24

3-
function setCookie(headers: Record<string, string> | undefined, name: string, value: string) {
5+
const scrypt = promisify(crypto.scrypt)
6+
7+
function setCookie(
8+
headers: Record<string, string> | undefined,
9+
name: string,
10+
value: string,
11+
options: { expires?: Date } = { expires: new Date(Date.now() + 1000 * 60 * 60 * 24) } // Default to 1 day expiration
12+
) {
13+
serialize
414
if (!headers) return
5-
headers['Set-Cookie'] = `${name}=${value}; Path=/; HttpOnly; SameSite=Strict`
15+
headers['Set-Cookie'] = serialize(name, value, options)
616
}
717

8-
function deleteCookie(headers: Record<string, string> | undefined, name: string) {
18+
function deleteCookie(headers: Record<string, string> | undefined, name: string, expiresAt?: Date) {
919
if (!headers) return
1020
headers['Set-Cookie'] = `${name}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`
1121
}
@@ -24,3 +34,16 @@ export function setSessionCookie(headers: Record<string, string> | undefined, va
2434
export function deleteSessionCookie(headers?: Record<string, string>) {
2535
deleteCookie(headers, SESSION_COOKIE_NAME)
2636
}
37+
38+
export async function hashPassword(password: string): Promise<string> {
39+
const salt = randomBytes(8).toString('hex')
40+
const derivedKey = await scrypt(password, salt, 64)
41+
return salt + ':' + (derivedKey as Buffer).toString('hex')
42+
}
43+
44+
export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
45+
const [salt, key] = hashedPassword.split(':')
46+
const keyBuffer = Buffer.from(key, 'hex')
47+
const derivedKey = await scrypt(password, salt, 64)
48+
return crypto.timingSafeEqual(keyBuffer, derivedKey as any)
49+
}

packages/next/src/components/collection-sidebar/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export async function AppSidebar({ collections }: { collections: Record<string,
2525
intent="inset"
2626
className="flex gap-y-12 p-0 py-6 group-[:not([data-sidebar-state=collapsed])]/sidebar-container:p-6"
2727
>
28-
<SidebarHeader className="border-border m-0 size-auto border-b pl-6">
28+
{/* TODO: Make sidebar variations, so we won't need an important css flag */}
29+
<SidebarHeader className="border-border m-0 size-auto! border-b pl-6 h-auto! w-full!">
2930
<div className="flex h-[68px] items-center gap-x-4">
3031
<div className="bg-primary/15 dark:bg-primary/20 border-primary dark:border-primary/40 relative flex overflow-clip rounded-md border p-4">
3132
<div className="bg-primary/20 absolute -inset-x-[25%] inset-y-0 m-auto h-2 -translate-x-4 -translate-y-4 -rotate-45 blur-[3px]" />

packages/next/src/config.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { AuthLayout } from './views/auth/layout'
1616
import { LoginView } from './views/auth/login'
1717
import { SignUpView } from './views/auth/sign-up'
1818
import { CreateView } from './views/collections/create'
19+
import { HomeView } from './views/collections/home'
1920
import { CollectionLayout } from './views/collections/layout'
2021
import { ListView } from './views/collections/list'
2122
import { OneView } from './views/collections/one'
@@ -68,6 +69,17 @@ export function defineNextJsServerConfig<
6869
): NextJsServerConfig<TFullSchema, TContext, TCollections, TApiRouter> {
6970
const radixRouter = createRouter<RouterData>()
7071

72+
radixRouter.insert('/collections', {
73+
requiredAuthentication: true,
74+
view(args: { serverConfig: ServerConfig }) {
75+
return (
76+
<CollectionLayout serverConfig={args.serverConfig}>
77+
<HomeView serverConfig={args.serverConfig} />
78+
</CollectionLayout>
79+
)
80+
},
81+
})
82+
7183
// Collection
7284
radixRouter.insert(`/collections/:slug`, {
7385
requiredAuthentication: true,

packages/next/src/pages/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export async function RootPage(props: RootProps) {
2727
if (result.requiredAuthentication) {
2828
user = await getUser(props.serverFunction)
2929
if (!user) {
30-
return <NotAuthorizedPage redirectURL="/admin/login" />
30+
return <NotAuthorizedPage redirectURL="/admin/auth/login" />
3131
}
3232
}
3333

packages/next/src/views/auth/login.client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function LoginClientForm() {
3131
return
3232
}
3333

34-
redirect('../collections/posts')
34+
redirect('../collections')
3535
}
3636

3737
return (

0 commit comments

Comments
 (0)