Skip to content

Commit 3f0405f

Browse files
feat(web): switch to react (Partial Implementation) (#353)
Co-authored-by: BlankParticle <[email protected]>
1 parent b290ec0 commit 3f0405f

File tree

98 files changed

+12047
-819
lines changed

Some content is hidden

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

98 files changed

+12047
-819
lines changed

.env.local.example

+6-1
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,9 @@ DB_REDIS_CONNECTION_STRING="redis://localhost:3901"
9292
MAIL_DOMAINS='{"free": ["free.localhost.email"], "premium": ["premium.localhost.email"], "fwd": ["fwd.uninbox.dev"]}'
9393
PRIMARY_DOMAIN='localhost'
9494

95-
UNKEY_ROOT_KEY=""
95+
UNKEY_ROOT_KEY=""
96+
97+
############################ NEXT_PUBLIC VARIABLES ############################
98+
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
99+
NEXT_PUBLIC_STORAGE_URL=http://localhost:3200
100+
NEXT_PUBLIC_PLATFORM_URL=http://localhost:3300

apps/platform/package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
},
1010
"dependencies": {
1111
"@simplewebauthn/server": "^9.0.3",
12-
"@trpc/client": "^10.45.2",
13-
"@trpc/server": "^10.45.2",
12+
"@trpc/client": "10.45.2",
13+
"@trpc/server": "10.45.2",
1414
"@u22n/database": "workspace:^",
1515
"@u22n/realtime": "workspace:^",
1616
"@u22n/tiptap": "workspace:^",
@@ -21,7 +21,8 @@
2121
"h3": "^1.11.1",
2222
"lucia": "^3.1.1",
2323
"nitropack": "2.9.4",
24-
"oslo": "^1.1.3"
24+
"oslo": "^1.1.3",
25+
"superjson": "^2.2.1"
2526
},
2627
"devDependencies": {
2728
"@simplewebauthn/types": "^9.0.1",

apps/platform/routes/auth/status.get.ts

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ type AuthStatusResponseType = {
66

77
export default eventHandler((event): AuthStatusResponseType => {
88
if (!event.context.account || !event.context.account.id) {
9-
deleteCookie(event, 'unsession');
109
return { authStatus: 'unauthenticated' };
1110
}
1211
return { authStatus: 'authenticated' };

apps/platform/trpc/createContext.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { inferAsyncReturnType } from '@trpc/server';
21
import type { H3Event } from 'h3';
32
import { db } from '@u22n/database';
43
import type { OrgContext, AccountContext } from '@u22n/types';
@@ -12,4 +11,4 @@ export const createContext = async (event: H3Event) => {
1211
return { db, account, org, event };
1312
};
1413

15-
export type Context = inferAsyncReturnType<typeof createContext>;
14+
export type Context = Awaited<ReturnType<typeof createContext>>;

apps/platform/trpc/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { addressRouter } from './routers/userRouter/addressRouter';
2020
import { defaultsRouter } from './routers/userRouter/defaultsRouter';
2121
import { twoFactorRouter } from './routers/authRouter/twoFactorRouter';
2222
import { securityRouter } from './routers/userRouter/securityRouter';
23+
import { storeRouter } from './routers/orgRouter/orgStoreRouter';
2324

2425
export const trpcPlatformContext = createContext;
2526

@@ -42,6 +43,7 @@ const trpcPlatformOrgSetupRouter = router({
4243
profile: orgProfileRouter,
4344
billing: billingRouter
4445
});
46+
4547
const trpcPlatformOrgUsersRouter = router({
4648
invites: invitesRouter,
4749
members: orgMembersRouter,
@@ -57,7 +59,8 @@ const trpcPlatformOrgRouter = router({
5759
contacts: contactsRouter,
5860
setup: trpcPlatformOrgSetupRouter,
5961
users: trpcPlatformOrgUsersRouter,
60-
mail: trpcPlatformOrgMailRouter
62+
mail: trpcPlatformOrgMailRouter,
63+
store: storeRouter
6164
});
6265

6366
export const trpcPlatformRouter = router({

apps/platform/trpc/routers/authRouter/passkeyRouter.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,12 @@ export const passkeyRouter = router({
2626
signUpWithPasskeyStart: publicRateLimitedProcedure.signUpPasskeyStart
2727
.input(
2828
z.object({
29-
username: zodSchemas.username(),
30-
authenticatorType: z.enum(['platform', 'cross-platform'])
29+
username: zodSchemas.username()
3130
})
3231
)
3332
.query(async ({ input, ctx }) => {
3433
const { db } = ctx;
35-
const { username, authenticatorType } = input;
34+
const { username } = input;
3635
const { available, error } = await validateUsername(db, input.username);
3736
if (!available) {
3837
throw new TRPCError({
@@ -45,8 +44,7 @@ export const passkeyRouter = router({
4544
const passkeyOptions = await usePasskeys.generateRegistrationOptions({
4645
userDisplayName: username,
4746
username: username,
48-
accountPublicId: publicId,
49-
authenticatorAttachment: authenticatorType
47+
accountPublicId: publicId
5048
});
5149
return { publicId, options: passkeyOptions };
5250
}),
@@ -210,6 +208,18 @@ export const passkeyRouter = router({
210208
id: true,
211209
publicId: true,
212210
username: true
211+
},
212+
with: {
213+
orgMemberships: {
214+
with: {
215+
org: {
216+
columns: {
217+
shortcode: true,
218+
id: true
219+
}
220+
}
221+
}
222+
}
213223
}
214224
});
215225

@@ -241,6 +251,9 @@ export const passkeyRouter = router({
241251
.set({ lastLoginAt: new Date() })
242252
.where(eq(accounts.id, account.id));
243253

244-
return { success: true };
254+
const defaultOrg = account.orgMemberships.sort((a, b) => a.id - b.id)[0]
255+
?.org.shortcode;
256+
257+
return { success: true, defaultOrg };
245258
})
246259
});

apps/platform/trpc/routers/authRouter/passwordRouter.ts

+121-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
strongPasswordSchema
1515
} from '@u22n/utils';
1616
import { TRPCError } from '@trpc/server';
17-
import { createError, setCookie } from 'h3';
17+
import { createError, deleteCookie, getCookie, setCookie } from 'h3';
1818
import { lucia } from '../../../utils/auth';
1919
import { validateUsername } from './signupRouter';
2020
import { createLuciaSessionCookie } from '../../../utils/session';
@@ -23,6 +23,9 @@ import { TOTPController } from 'oslo/otp';
2323
import { useStorage, useRuntimeConfig } from '#imports';
2424

2525
export const passwordRouter = router({
26+
/**
27+
* @deprecated remove with Nuxt Webapp
28+
*/
2629
signUpWithPassword: publicRateLimitedProcedure.signUpWithPassword
2730
.input(
2831
z.object({
@@ -77,6 +80,106 @@ export const passwordRouter = router({
7780
return { success: true };
7881
}),
7982

83+
signUpWithPassword2FA: publicRateLimitedProcedure.signUpWithPassword
84+
.input(
85+
z.object({
86+
username: zodSchemas.username(),
87+
password: strongPasswordSchema,
88+
twoFactorCode: z.string().min(6).max(6)
89+
})
90+
)
91+
.mutation(async ({ ctx, input }) => {
92+
const { username, password, twoFactorCode } = input;
93+
const { db } = ctx;
94+
95+
const twoFaChallengeCookie = getCookie(ctx.event, 'un-2fa-challenge');
96+
if (!twoFaChallengeCookie) {
97+
return {
98+
success: false,
99+
error: '2FA cookie not found or expired, Please try to setup new 2FA'
100+
};
101+
}
102+
103+
const authStorage = useStorage('auth');
104+
const twoFaChallenge = await authStorage.getItem(
105+
`un2faChallenge:${username}-${twoFaChallengeCookie}`
106+
);
107+
108+
if (typeof twoFaChallenge !== 'string') {
109+
return {
110+
success: false,
111+
error:
112+
'2FA challenge was invalid or expired, Please try to setup new 2FA'
113+
};
114+
}
115+
116+
const secret = decodeHex(twoFaChallenge);
117+
const isValid = await new TOTPController().verify(twoFactorCode, secret);
118+
119+
if (!isValid) {
120+
return {
121+
success: false,
122+
error: 'Invalid 2FA code'
123+
};
124+
}
125+
126+
const { accountId, publicId, recoveryCode } = await db.transaction(
127+
async (tx) => {
128+
try {
129+
// making sure someone doesn't bypass the client side validation
130+
const { available, error } = await validateUsername(tx, username);
131+
if (!available) {
132+
throw new TRPCError({
133+
code: 'FORBIDDEN',
134+
message: `Username Error : ${error}`
135+
});
136+
}
137+
138+
const passwordHash = await new Argon2id().hash(password);
139+
const publicId = typeIdGenerator('account');
140+
141+
const recoveryCode = nanoIdToken();
142+
const hashedRecoveryCode = await new Argon2id().hash(recoveryCode);
143+
144+
const newUser = await tx.insert(accounts).values({
145+
username,
146+
publicId,
147+
passwordHash,
148+
twoFactorEnabled: true,
149+
twoFactorSecret: twoFaChallenge,
150+
recoveryCode: hashedRecoveryCode
151+
});
152+
153+
return {
154+
accountId: Number(newUser.insertId),
155+
publicId,
156+
recoveryCode
157+
};
158+
} catch (err) {
159+
tx.rollback();
160+
console.error(err);
161+
throw err;
162+
}
163+
}
164+
);
165+
166+
const cookie = await createLuciaSessionCookie(ctx.event, {
167+
accountId,
168+
username,
169+
publicId
170+
});
171+
172+
setCookie(ctx.event, cookie.name, cookie.value, cookie.attributes);
173+
deleteCookie(ctx.event, 'un-2fa-challenge');
174+
175+
await db
176+
.update(accounts)
177+
.set({ lastLoginAt: new Date() })
178+
.where(eq(accounts.id, accountId));
179+
180+
return { success: true, error: null, recoveryCode };
181+
}),
182+
80183
signInWithPassword: publicRateLimitedProcedure.signInWithPassword
81184
.input(
82185
z.object({
@@ -98,6 +201,18 @@ export const passwordRouter = router({
98201
passwordHash: true,
99202
twoFactorSecret: true,
100203
twoFactorEnabled: true
204+
},
205+
with: {
206+
orgMemberships: {
207+
with: {
208+
org: {
209+
columns: {
210+
shortcode: true,
211+
id: true
212+
}
213+
}
214+
}
215+
}
101216
}
102217
});
103218

@@ -192,7 +307,11 @@ export const passwordRouter = router({
192307
.set({ lastLoginAt: new Date() })
193308
.where(eq(accounts.id, userResponse.id));
194309

195-
return { success: true };
310+
const defaultOrg = userResponse.orgMemberships.sort(
311+
(a, b) => a.id - b.id
312+
)[0]?.org.shortcode;
313+
314+
return { success: true, defaultOrg };
196315
}
197316

198317
throw new TRPCError({

apps/platform/trpc/routers/authRouter/signupRouter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export async function validateUsername(
3333
return {
3434
available: false,
3535
error:
36-
'This username is currently reserved. If you own this trademark, please Contact Support'
36+
'This username is reserved. If you own this trademark, please contact support'
3737
};
3838
}
3939
return {

apps/platform/trpc/routers/authRouter/twoFactorRouter.ts

+54-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import { z } from 'zod';
2-
import { router, accountProcedure } from '../../trpc';
2+
import {
3+
router,
4+
accountProcedure,
5+
publicRateLimitedProcedure
6+
} from '../../trpc';
37
import { eq } from '@u22n/database/orm';
48
import { accounts } from '@u22n/database/schema';
59
import { decodeHex, encodeHex } from 'oslo/encoding';
610
import { TOTPController, createTOTPKeyURI } from 'oslo/otp';
711
import { TRPCError } from '@trpc/server';
8-
import { nanoIdToken } from '@u22n/utils';
12+
import { nanoIdToken, zodSchemas } from '@u22n/utils';
913
import { Argon2id } from 'oslo/password';
14+
import { setCookie, getCookie } from 'h3';
15+
import { useRuntimeConfig, useStorage } from '#imports';
1016

1117
export const twoFactorRouter = router({
18+
/**
19+
* @deprecated remove with Nuxt Webapp
20+
*/
1221
createTwoFactorSecret: accountProcedure
1322
.input(z.object({}).strict())
1423
.mutation(async ({ ctx }) => {
@@ -51,6 +60,9 @@ export const twoFactorRouter = router({
5160
return { uri };
5261
}),
5362

63+
/**
64+
* @deprecated remove with Nuxt Webapp
65+
*/
5466
verifyTwoFactor: accountProcedure
5567
.input(
5668
z
@@ -118,6 +130,10 @@ export const twoFactorRouter = router({
118130

119131
return { recoveryCode: recoveryCode };
120132
}),
133+
134+
/**
135+
* @deprecated remove with Nuxt Webapp
136+
*/
121137
disableTwoFactor: accountProcedure
122138
.input(z.object({ twoFactorCode: z.string() }).strict())
123139
.mutation(async ({ ctx, input }) => {
@@ -167,5 +183,41 @@ export const twoFactorRouter = router({
167183
})
168184
.where(eq(accounts.id, accountId));
169185
return {};
186+
}),
187+
188+
createTwoFactorChallenge: publicRateLimitedProcedure.createTwoFactorChallenge
189+
.input(z.object({ username: zodSchemas.username() }))
190+
.query(async ({ ctx, input }) => {
191+
const authStorage = useStorage('auth');
192+
const existingChallenge = getCookie(ctx.event, 'un-2fa-challenge');
193+
194+
if (existingChallenge) {
195+
const existingSecret = await authStorage.getItem(
196+
`un2faChallenge:${input.username}-${existingChallenge}`
197+
);
198+
if (typeof existingSecret === 'string') {
199+
return {
200+
uri: createTOTPKeyURI(
201+
'UnInbox.com',
202+
input.username,
203+
decodeHex(existingSecret)
204+
)
205+
};
206+
}
207+
}
208+
209+
const newSecret = crypto.getRandomValues(new Uint8Array(20));
210+
const uri = createTOTPKeyURI('UnInbox.com', input.username, newSecret);
211+
const hexSecret = encodeHex(newSecret);
212+
const challengeId = nanoIdToken();
213+
await authStorage.setItem(
214+
`un2faChallenge:${input.username}-${challengeId}`,
215+
hexSecret
216+
);
217+
setCookie(ctx.event, 'un-2fa-challenge', challengeId, {
218+
domain: useRuntimeConfig().primaryDomain,
219+
httpOnly: true
220+
});
221+
return { uri };
170222
})
171223
});

0 commit comments

Comments
 (0)