One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links ๐Ÿ“… calendar.xyehr.cn
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #222 from EvanTechDev/feature/upgrade-clerk-from-next.js-v6-to-v7

Upgrade Clerk Next.js integration from v6 to v7 APIs

authored by

Evan Huang and committed by
GitHub
e6cd13e2 2dfba17f

+135 -76
+4 -2
app/layout.tsx
··· 66 66 <ClerkProvider 67 67 publishableKey={clerkPublishableKey} 68 68 localization={enUS} 69 - fallbackRedirectUrl="/" 70 - forceRedirectUrl="/" 69 + signInFallbackRedirectUrl="/app" 70 + signUpFallbackRedirectUrl="/app" 71 + signInForceRedirectUrl="/" 72 + signUpForceRedirectUrl="/" 71 73 signInUrl="/sign-in" 72 74 signUpUrl="/sign-up" 73 75 >
+28 -12
components/auth/login-form.tsx
··· 20 20 className, 21 21 ...props 22 22 }: React.ComponentPropsWithoutRef<"div">) { 23 - const { signIn, setActive } = useSignIn(); 23 + const { signIn } = useSignIn(); 24 24 const [email, setEmail] = useState(""); 25 25 const [password, setPassword] = useState(""); 26 26 const [isLoading, setIsLoading] = useState(false); ··· 74 74 setError(""); 75 75 76 76 try { 77 - const result = await signIn.create({ 78 - identifier: email, 77 + const { error } = await signIn.password({ 78 + emailAddress: email, 79 79 password, 80 80 }); 81 + if (error) { 82 + setError(error.longMessage || error.message || "Login failed. Please try again."); 83 + return; 84 + } 81 85 82 - if (result.status === "complete") { 83 - await setActive({ session: result.createdSessionId }); 84 - router.push("/"); 86 + if (signIn.status === "complete") { 87 + const { error: finalizeError } = await signIn.finalize({ 88 + navigate: ({ decorateUrl }) => { 89 + const url = decorateUrl("/"); 90 + if (url.startsWith("http")) { 91 + window.location.href = url; 92 + return; 93 + } 94 + router.push(url); 95 + }, 96 + }); 97 + if (finalizeError) { 98 + setError(finalizeError.longMessage || finalizeError.message || "Login failed. Please try again."); 99 + } 85 100 } 86 101 } catch (err: any) { 87 102 setError(err.errors?.[0]?.longMessage || "Login failed. Please try again."); ··· 102 117 setError("Please complete the CAPTCHA verification."); 103 118 return; 104 119 } 105 - signIn.authenticateWithRedirect({ 120 + signIn.sso({ 106 121 strategy, 107 - redirectUrl: "/sign-in/sso-callback", 108 - redirectUrlComplete: "/", 122 + redirectUrl: "/", 123 + redirectCallbackUrl: "/sign-in/sso-callback", 109 124 }); 110 125 }; 111 126 112 127 const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; 128 + const hasCaptcha = Boolean(siteKey); 113 129 114 130 return ( 115 131 <div className={cn("flex flex-col gap-6", className)} {...props}> ··· 203 219 onChange={(e) => setPassword(e.target.value)} 204 220 /> 205 221 </div> 206 - {siteKey && ( 222 + {hasCaptcha && ( 207 223 <div className="turnstile-container"> 208 224 <Turnstile 209 225 ref={turnstileRef} 210 - siteKey={siteKey} 226 + siteKey={siteKey!} 211 227 onSuccess={handleTurnstileSuccess} 212 228 onError={() => { 213 229 console.error("Turnstile widget error"); ··· 224 240 <Button 225 241 type="submit" 226 242 className="w-full bg-[#0066ff] hover:bg-[#0047cc] text-white" 227 - disabled={siteKey && (!isCaptchaCompleted || isLoading)} 243 + disabled={hasCaptcha ? !isCaptchaCompleted || isLoading : isLoading} 228 244 > 229 245 {isLoading ? "Signing in..." : "Sign in"} 230 246 </Button>
+41 -20
components/auth/reset-form.tsx
··· 21 21 className, 22 22 ...props 23 23 }: React.ComponentPropsWithoutRef<"div">) { 24 - const { isLoaded, signIn } = useSignIn(); 24 + const { signIn } = useSignIn(); 25 25 const router = useRouter(); 26 26 const [step, setStep] = useState<"email" | "code" | "password">("email"); 27 27 const [formData, setFormData] = useState({ ··· 82 82 setError(""); 83 83 84 84 try { 85 - if (!isLoaded || !signIn) { 86 - return; 87 - } 88 85 if (step === "email") { 89 - await signIn.create({ 90 - strategy: "reset_password_email_code", 91 - identifier: formData.email, 92 - }); 86 + const { error: createError } = await signIn.create({ identifier: formData.email }); 87 + if (createError) { 88 + setError(createError.longMessage || createError.message || "An error occurred. Please try again."); 89 + return; 90 + } 91 + const { error: sendCodeError } = await signIn.resetPasswordEmailCode.sendCode(); 92 + if (sendCodeError) { 93 + setError(sendCodeError.longMessage || sendCodeError.message || "An error occurred. Please try again."); 94 + return; 95 + } 93 96 setStep("code"); 94 97 } else if (step === "code") { 95 - const result = await signIn.attemptFirstFactor({ 96 - strategy: "reset_password_email_code", 98 + const { error: verifyCodeError } = await signIn.resetPasswordEmailCode.verifyCode({ 97 99 code: formData.code, 98 100 }); 99 - if (result?.status === "needs_new_password") { 101 + if (verifyCodeError) { 102 + setError(verifyCodeError.longMessage || verifyCodeError.message || "An error occurred. Please try again."); 103 + return; 104 + } 105 + if (signIn.status === "needs_new_password") { 100 106 setStep("password"); 101 107 } 102 108 } else { 103 - const result = await signIn.resetPassword({ 109 + const { error: submitPasswordError } = await signIn.resetPasswordEmailCode.submitPassword({ 104 110 password: formData.password, 105 111 }); 106 - if (result?.status === "complete") { 107 - router.push("/app"); 112 + if (submitPasswordError) { 113 + setError(submitPasswordError.longMessage || submitPasswordError.message || "An error occurred. Please try again."); 114 + return; 115 + } 116 + if (signIn.status === "complete") { 117 + const { error: finalizeError } = await signIn.finalize({ 118 + navigate: ({ decorateUrl }) => { 119 + const url = decorateUrl("/app"); 120 + if (url.startsWith("http")) { 121 + window.location.href = url; 122 + return; 123 + } 124 + router.push(url); 125 + }, 126 + }); 127 + if (finalizeError) { 128 + setError(finalizeError.longMessage || finalizeError.message || "An error occurred. Please try again."); 129 + } 108 130 } 109 131 } 110 132 } catch (err: any) { ··· 198 220 199 221 const stepContent = getStepContent(); 200 222 const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; 223 + const hasCaptcha = Boolean(siteKey); 201 224 202 225 return ( 203 226 <div className={cn("flex flex-col gap-6", className)} {...props}> ··· 211 234 <div className="grid gap-6"> 212 235 {stepContent.fields} 213 236 214 - {step === "email" && siteKey && ( 237 + {step === "email" && hasCaptcha && ( 215 238 <div className="turnstile-container"> 216 239 <Turnstile 217 240 ref={turnstileRef} 218 - siteKey={siteKey} 241 + siteKey={siteKey!} 219 242 onSuccess={handleTurnstileSuccess} 220 243 onError={() => { 221 244 setIsCaptchaCompleted(false); ··· 240 263 type="submit" 241 264 className="w-full bg-[#0066ff] hover:bg-[#0047cc] text-white" 242 265 disabled={ 243 - !isLoaded 244 - ? true 245 - : siteKey && step === "email" 266 + hasCaptcha && step === "email" 246 267 ? !isCaptchaCompleted || isLoading 247 268 : isLoading 248 269 } 249 270 > 250 - {!isLoaded ? "Loading auth..." : isLoading ? "Processing..." : stepContent.buttonText} 271 + {isLoading ? "Processing..." : stepContent.buttonText} 251 272 </Button> 252 273 253 274 <div className="text-center text-sm">
+42 -26
components/auth/sign-up-form.tsx
··· 2 2 3 3 import { Button } from "@/components/ui/button"; 4 4 import { cn } from "@/lib/utils"; 5 - import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 5 + import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; 6 6 import { Input } from "@/components/ui/input"; 7 7 import { Label } from "@/components/ui/label"; 8 8 import { useSignUp } from "@clerk/nextjs"; ··· 14 14 className, 15 15 ...props 16 16 }: React.ComponentPropsWithoutRef<"div">) { 17 - const { isLoaded, signUp, setActive } = useSignUp(); 17 + const { signUp } = useSignUp(); 18 18 const router = useRouter(); 19 19 const [step, setStep] = useState<"initial" | "verification">("initial"); 20 20 const [formData, setFormData] = useState({ ··· 135 135 setError("Please complete the CAPTCHA verification."); 136 136 return; 137 137 } 138 - if (!isLoaded || !signUp) { 139 - return; 140 - } 141 138 signUp 142 - .authenticateWithRedirect({ 139 + .sso({ 143 140 strategy, 144 - redirectUrl: "/sign-up/sso-callback", 145 - redirectUrlComplete: "/app", 141 + redirectUrl: "/app", 142 + redirectCallbackUrl: "/sign-up/sso-callback", 146 143 }) 147 144 .catch((err: any) => { 148 145 setError(err.errors?.[0]?.longMessage || "OAuth sign up failed. Please try again."); ··· 160 157 setError(""); 161 158 162 159 try { 163 - if (!isLoaded || !signUp) { 164 - return; 165 - } 166 - 167 160 if (step === "initial") { 168 161 if (!isEmailDomainAllowed(formData.email)) { 169 162 setError( ··· 173 166 return; 174 167 } 175 168 176 - await signUp.create({ 169 + const { error: passwordError } = await signUp.password({ 177 170 firstName: formData.firstName, 178 171 lastName: formData.lastName, 179 172 emailAddress: formData.email, 180 173 password: formData.password, 181 174 }); 182 - await signUp.prepareEmailAddressVerification(); 175 + if (passwordError) { 176 + setError( 177 + passwordError.longMessage || passwordError.message || "An error occurred. Please try again.", 178 + ); 179 + return; 180 + } 181 + await signUp.verifications.sendEmailCode(); 183 182 setStep("verification"); 184 183 } else { 185 - const completeSignUp = await signUp.attemptEmailAddressVerification({ 184 + const { error: verifyError } = await signUp.verifications.verifyEmailCode({ 186 185 code: formData.code, 187 186 }); 188 - if (completeSignUp.status === "complete") { 189 - await setActive({ session: completeSignUp.createdSessionId }); 190 - router.push("/app"); 187 + if (verifyError) { 188 + setError( 189 + verifyError.longMessage || verifyError.message || "An error occurred. Please try again.", 190 + ); 191 + return; 192 + } 193 + if (signUp.status === "complete") { 194 + const { error: finalizeError } = await signUp.finalize({ 195 + navigate: ({ decorateUrl }) => { 196 + const url = decorateUrl("/app"); 197 + if (url.startsWith("http")) { 198 + window.location.href = url; 199 + return; 200 + } 201 + router.push(url); 202 + }, 203 + }); 204 + if (finalizeError) { 205 + setError( 206 + finalizeError.longMessage || finalizeError.message || "An error occurred. Please try again.", 207 + ); 208 + } 191 209 } 192 210 } 193 211 } catch (err: any) { ··· 251 269 type="button" 252 270 onClick={async () => { 253 271 try { 254 - await signUp?.prepareEmailAddressVerification(); 272 + await signUp.verifications.sendEmailCode(); 255 273 } catch (err) { 256 274 setError("Failed to resend code. Please try again."); 257 275 } ··· 271 289 } 272 290 273 291 const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; 292 + const hasCaptcha = Boolean(siteKey); 274 293 275 294 return ( 276 295 <div className={cn("flex flex-col gap-6", className)} {...props}> ··· 286 305 variant="outline" 287 306 className="w-full" 288 307 type="button" 289 - disabled={!isLoaded} 290 308 onClick={() => handleOAuthSignUp("oauth_microsoft")} 291 309 > 292 310 <svg ··· 306 324 variant="outline" 307 325 className="w-full" 308 326 type="button" 309 - disabled={!isLoaded} 310 327 onClick={() => handleOAuthSignUp("oauth_google")} 311 328 > 312 329 <svg ··· 339 356 variant="outline" 340 357 className="w-full" 341 358 type="button" 342 - disabled={!isLoaded} 343 359 onClick={() => handleOAuthSignUp("oauth_github")} 344 360 > 345 361 <svg ··· 412 428 value={formData.password} 413 429 onChange={handleChange} 414 430 /> 415 - {siteKey ? ( 431 + {hasCaptcha ? ( 416 432 <div className="turnstile-container"> 417 433 <Turnstile 418 434 ref={turnstileRef} 419 - siteKey={siteKey} 435 + siteKey={siteKey!} 420 436 onSuccess={handleTurnstileSuccess} 421 437 onError={() => { 422 438 console.error("Turnstile widget error"); ··· 442 458 <Button 443 459 type="submit" 444 460 className="w-full bg-[#0066ff] hover:bg-[#0047cc] text-white" 445 - disabled={!isLoaded || (siteKey && (!isCaptchaCompleted || isLoading))} 461 + disabled={hasCaptcha ? !isCaptchaCompleted || isLoading : isLoading} 446 462 > 447 - {!isLoaded ? "Loading auth..." : isLoading ? "Creating account..." : "Create account"} 463 + {isLoading ? "Creating account..." : "Create account"} 448 464 </Button> 449 465 </div> 450 466
+1 -1
package.json
··· 12 12 }, 13 13 "dependencies": { 14 14 "@clerk/localizations": "4.3.0", 15 - "@clerk/nextjs": "6.39.1", 15 + "@clerk/nextjs": "^7.0.0", 16 16 "@hookform/resolvers": "5.2.2", 17 17 "@marsidev/react-turnstile": "latest", 18 18 "@radix-ui/react-accordion": "latest",
+19 -15
proxy.ts
··· 1 - import { clerkMiddleware } from "@clerk/nextjs/server" 1 + import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server" 2 2 3 - export default clerkMiddleware({ 4 - publicRoutes: [ 5 - "/", 6 - "/app", 7 - "/sign-in", 8 - "/sign-up", 9 - "/reset-password", 10 - "/api/share", 11 - "/api/verify", 12 - "/at-oauth", 13 - "/api/atproto/(.*)", 14 - "/oauth-client-metadata.json", 15 - "/api/share/public" 16 - ], 3 + const isPublicRoute = createRouteMatcher([ 4 + "/", 5 + "/app", 6 + "/sign-in(.*)", 7 + "/sign-up(.*)", 8 + "/reset-password", 9 + "/api/share", 10 + "/api/verify", 11 + "/at-oauth", 12 + "/api/atproto/(.*)", 13 + "/oauth-client-metadata.json", 14 + "/api/share/public", 15 + ]) 16 + 17 + export default clerkMiddleware(async (auth, req) => { 18 + if (!isPublicRoute(req)) { 19 + await auth.protect() 20 + } 17 21 }) 18 22 19 23 export const config = {