Compare commits
15 Commits
5dc33897ce
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b486c4f3fe | |||
|
|
bc02da9d48 | ||
| 7e474a85a2 | |||
| 8f8fcb83c9 | |||
| 936000b32a | |||
| 5315f1b188 | |||
| d6d54df384 | |||
| f1c880690f | |||
| 9fee97dea6 | |||
| 789dc380c1 | |||
| fe94d4c543 | |||
| f8f59a5134 | |||
| 3d1336c3b2 | |||
| 05eb117e4b | |||
| 84728c9532 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
.astro/
|
.astro/
|
||||||
|
.wrangler/
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { defineConfig } from 'astro/config';
|
|||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import sitemap from '@astrojs/sitemap';
|
import sitemap from '@astrojs/sitemap';
|
||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react';
|
||||||
import node from '@astrojs/node';
|
import cloudflare from '@astrojs/cloudflare';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: 'server',
|
output: 'server',
|
||||||
adapter: node({ mode: 'standalone' }),
|
adapter: cloudflare(),
|
||||||
site: 'https://cachetdeco.com',
|
site: 'https://cachetdeco.com',
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: 'fr',
|
defaultLocale: 'fr',
|
||||||
@@ -26,5 +26,8 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
|
ssr: {
|
||||||
|
noExternal: ['motion', 'motion/react'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
76
docs/cloudflare-workers-astro-v6.md
Normal file
76
docs/cloudflare-workers-astro-v6.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Cloudflare Workers + Astro v6 Deployment Notes
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
- Astro v6 + `@astrojs/cloudflare` adapter (v13+)
|
||||||
|
- Deployed as **Cloudflare Worker** (not Pages)
|
||||||
|
- Build: `npm run build` → Deploy: `npx wrangler deploy`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 1 — `wrangler.json` with `pages_build_output_dir` breaks deploy
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
The name 'ASSETS' is reserved in Pages projects. Please use a different name for your Assets binding.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause:** Having `pages_build_output_dir` in `wrangler.json` makes Wrangler treat the project as a Pages project. The adapter generates `dist/server/wrangler.json` with an `ASSETS` binding for static assets, which is reserved in Pages context.
|
||||||
|
|
||||||
|
**Fix:** Remove `pages_build_output_dir` from `wrangler.json`, or delete the file entirely. The adapter generates a complete `dist/server/wrangler.json` — Wrangler picks it up automatically via config redirect.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 2 — `locals.runtime.env` removed in Astro v6
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
Astro.locals.runtime.env has been removed in Astro v6.
|
||||||
|
Use 'import { env } from "cloudflare:workers"' instead.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause:** Astro v6 dropped the `locals.runtime.env` pattern for accessing Cloudflare env vars/bindings.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ❌ Astro v5 (broken in v6)
|
||||||
|
const runtime = (locals as any).runtime;
|
||||||
|
const apiKey = runtime?.env?.RESEND_API_KEY;
|
||||||
|
|
||||||
|
// ✅ Astro v6
|
||||||
|
import { env } from 'cloudflare:workers';
|
||||||
|
const apiKey = (env as unknown as Record<string, string>).RESEND_API_KEY;
|
||||||
|
```
|
||||||
|
|
||||||
|
Install types and add to `tsconfig.json`:
|
||||||
|
```bash
|
||||||
|
npm install --save-dev @cloudflare/workers-types
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["@cloudflare/workers-types"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Env vars in Cloudflare Workers dashboard
|
||||||
|
|
||||||
|
- Set vars under **Worker → Settings → Variables and Secrets**
|
||||||
|
- They are accessible via `env` from `cloudflare:workers` at runtime
|
||||||
|
- Do **not** rely on `process.env` or `import.meta.env` for server-side secrets in Workers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wrangler config redirect (how it works)
|
||||||
|
|
||||||
|
When `astro build` runs, `@astrojs/cloudflare` generates `dist/server/wrangler.json`. Wrangler detects this and logs:
|
||||||
|
```
|
||||||
|
Using redirected Wrangler configuration.
|
||||||
|
- Configuration being used: "dist/server/wrangler.json"
|
||||||
|
- Original user's configuration: "wrangler.json"
|
||||||
|
```
|
||||||
|
The generated config includes the worker `main` entry, `assets` binding, `compatibility_date`, and any adapter-detected bindings (e.g. `IMAGES`, `SESSION`).
|
||||||
893
package-lock.json
generated
893
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
|||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^10.0.4",
|
"@astrojs/cloudflare": "^13.1.7",
|
||||||
"@astrojs/react": "^5.0.2",
|
"@astrojs/react": "^5.0.2",
|
||||||
"@astrojs/sitemap": "^3.7.2",
|
"@astrojs/sitemap": "^3.7.2",
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
"tw-animate-css": "^1.4.0"
|
"tw-animate-css": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "^4.20260403.1",
|
||||||
"@types/react": "^19.2.14"
|
"@types/react": "^19.2.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
backend:
|
backend:
|
||||||
name: github
|
name: github
|
||||||
repo: mbcube/cachetdeco # REPLACE with your GitHub org/repo
|
repo: mbcube/cachetdeco
|
||||||
branch: main
|
branch: cloudflare
|
||||||
# For Cloudflare Pages (no Netlify Identity), use PKCE flow:
|
base_url: https://cachetdeco.br-mouad.workers.dev
|
||||||
auth_type: pkce
|
auth_endpoint: /api/auth
|
||||||
app_id: YOUR_GITHUB_OAUTH_APP_CLIENT_ID # REPLACE with your GitHub OAuth App client ID
|
|
||||||
|
|
||||||
locale: fr
|
locale: fr
|
||||||
media_folder: "public/images"
|
media_folder: "public/images"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const legalNotice = footer.legal.replace('{year}', String(year));
|
|||||||
---
|
---
|
||||||
|
|
||||||
<FooterAnimated
|
<FooterAnimated
|
||||||
client:visible
|
client:load
|
||||||
siteName={general.siteName}
|
siteName={general.siteName}
|
||||||
serviceArea={general.serviceArea}
|
serviceArea={general.serviceArea}
|
||||||
phone={general.phone}
|
phone={general.phone}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { Dialog, DialogPanel } from "@headlessui/react";
|
import { Dialog, DialogPanel } from "@headlessui/react";
|
||||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
@@ -16,18 +16,49 @@ export default function Header({
|
|||||||
ctaHref: string;
|
ctaHref: string;
|
||||||
}) {
|
}) {
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
const lastScrollY = useRef(0);
|
||||||
const phoneHref = `tel:${general.phone.replace(/\D/g, "")}`;
|
const phoneHref = `tel:${general.phone.replace(/\D/g, "")}`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const currentY = window.scrollY;
|
||||||
|
const delta = currentY - lastScrollY.current;
|
||||||
|
if (delta > 8 && currentY > 80) {
|
||||||
|
setIsVisible(false);
|
||||||
|
} else if (delta < -8) {
|
||||||
|
setIsVisible(true);
|
||||||
|
}
|
||||||
|
setIsScrolled(currentY > 10);
|
||||||
|
lastScrollY.current = currentY;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync state with the actual scroll position on mount
|
||||||
|
const initialY = window.scrollY;
|
||||||
|
lastScrollY.current = initialY;
|
||||||
|
setIsScrolled(initialY > 10);
|
||||||
|
if (initialY > 80) {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="absolute inset-x-0 top-0 z-50">
|
<header
|
||||||
|
className={`fixed inset-x-0 top-0 z-50 transition-all duration-300 ease-in-out`}
|
||||||
|
style={{ transform: isVisible ? "translateY(0)" : "translateY(-100%)" }}
|
||||||
|
>
|
||||||
<nav
|
<nav
|
||||||
aria-label="Global"
|
aria-label="Global"
|
||||||
className="flex items-center justify-between p-6 lg:px-8"
|
className={`flex items-center justify-between p-6 lg:px-8 ${isScrolled ? "bg-[#314732]/95 shadow-md backdrop-blur-sm py-4" : ""} `}
|
||||||
>
|
>
|
||||||
<div className="flex lg:flex-1">
|
<div className="flex lg:flex-1">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
className="flex items-center justify-center bg-[white] rounded-full shadow-md p-2 lg:translate-y-1.5"
|
className="flex items-center justify-center bg-[white] rounded-full shadow-md p-2"
|
||||||
>
|
>
|
||||||
<div className="rounded-full ">
|
<div className="rounded-full ">
|
||||||
<span className="sr-only">{general.siteName}</span>
|
<span className="sr-only">{general.siteName}</span>
|
||||||
@@ -56,7 +87,7 @@ export default function Header({
|
|||||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||||
key={item.name}
|
key={item.name}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-sm/6 font-semibold text-white hover:underline underline-offset-4 decoration-2 drop-shadow"
|
className="text-base/6 font-semibold text-white hover:underline underline-offset-4 decoration-2 drop-shadow"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</motion.a>
|
</motion.a>
|
||||||
|
|||||||
@@ -92,45 +92,45 @@ export default function Contact({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative isolate bg-brand-50/50 dark:bg-brand-900">
|
<div className="relative isolate bg-brand-50/50 dark:bg-brand-900">
|
||||||
|
<div className="absolute inset-y-0 left-0 -z-10 w-full overflow-hidden bg-brand-50 ring-1 ring-brand-900/10 lg:w-1/2 dark:bg-brand-900 dark:ring-white/10">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 size-full mask-[radial-gradient(100%_100%_at_top_right,white,transparent)] stroke-brand-200 dark:stroke-white/10"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
x="100%"
|
||||||
|
y={-1}
|
||||||
|
id="83fd4e5a-9d52-42fc-97b6-718e5d7ee527"
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<path d="M130 200V.5M.5 .5H200" fill="none" />
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" strokeWidth={0} className="fill-white dark:fill-brand-900" />
|
||||||
|
<svg x="100%" y={-1} className="overflow-visible fill-brand-50 dark:fill-brand-800/20">
|
||||||
|
<path d="M-470.5 0h201v201h-201Z" strokeWidth={0} />
|
||||||
|
</svg>
|
||||||
|
<rect fill="url(#83fd4e5a-9d52-42fc-97b6-718e5d7ee527)" width="100%" height="100%" strokeWidth={0} />
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute top-[calc(100%-13rem)] -left-56 hidden transform-gpu blur-3xl lg:top-[calc(50%-7rem)] lg:left-[max(-14rem,calc(100%-59rem))] dark:block"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
clipPath:
|
||||||
|
'polygon(74.1% 56.1%, 100% 38.6%, 97.5% 73.3%, 85.5% 100%, 80.7% 98.2%, 72.5% 67.7%, 60.2% 37.8%, 52.4% 32.2%, 47.5% 41.9%, 45.2% 65.8%, 27.5% 23.5%, 0.1% 35.4%, 17.9% 0.1%, 27.6% 23.5%, 76.1% 2.6%, 74.1% 56.1%)',
|
||||||
|
}}
|
||||||
|
className="aspect-1155/678 w-288.75 bg-linear-to-br from-brand-300 to-brand-600 opacity-10 dark:opacity-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="mx-auto grid max-w-7xl grid-cols-1 lg:grid-cols-2">
|
<div className="mx-auto grid max-w-7xl grid-cols-1 lg:grid-cols-2">
|
||||||
<motion.div {...slideIn("left", 0.05)} className="relative px-6 pt-12 pb-20 lg:static">
|
<motion.div {...slideIn("left", 0.05)} className="relative px-6 pt-12 pb-20 lg:static">
|
||||||
<div className="mx-auto max-w-xl lg:mx-0 lg:max-w-lg">
|
<div className="mx-auto max-w-xl lg:mx-0 lg:max-w-lg">
|
||||||
<div className="absolute inset-y-0 left-0 -z-10 w-full overflow-hidden bg-brand-50 ring-1 ring-brand-900/10 lg:w-1/2 dark:bg-brand-900 dark:ring-white/10">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-0 size-full mask-[radial-gradient(100%_100%_at_top_right,white,transparent)] stroke-brand-200 dark:stroke-white/10"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<pattern
|
|
||||||
x="100%"
|
|
||||||
y={-1}
|
|
||||||
id="83fd4e5a-9d52-42fc-97b6-718e5d7ee527"
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
patternUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<path d="M130 200V.5M.5 .5H200" fill="none" />
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect width="100%" height="100%" strokeWidth={0} className="fill-white dark:fill-brand-900" />
|
|
||||||
<svg x="100%" y={-1} className="overflow-visible fill-brand-50 dark:fill-brand-800/20">
|
|
||||||
<path d="M-470.5 0h201v201h-201Z" strokeWidth={0} />
|
|
||||||
</svg>
|
|
||||||
<rect fill="url(#83fd4e5a-9d52-42fc-97b6-718e5d7ee527)" width="100%" height="100%" strokeWidth={0} />
|
|
||||||
</svg>
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute top-[calc(100%-13rem)] -left-56 hidden transform-gpu blur-3xl lg:top-[calc(50%-7rem)] lg:left-[max(-14rem,calc(100%-59rem))] dark:block"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
clipPath:
|
|
||||||
'polygon(74.1% 56.1%, 100% 38.6%, 97.5% 73.3%, 85.5% 100%, 80.7% 98.2%, 72.5% 67.7%, 60.2% 37.8%, 52.4% 32.2%, 47.5% 41.9%, 45.2% 65.8%, 27.5% 23.5%, 0.1% 35.4%, 17.9% 0.1%, 27.6% 23.5%, 76.1% 2.6%, 74.1% 56.1%)',
|
|
||||||
}}
|
|
||||||
className="aspect-1155/678 w-288.75 bg-linear-to-br from-brand-300 to-brand-600 opacity-10 dark:opacity-20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<motion.dl {...fadeUp(0.1)} className="mt-10 space-y-4 text-base/7 text-brand-700 dark:text-brand-100">
|
<motion.dl {...fadeUp(0.1)} className="mt-10 space-y-4 text-base/7 text-brand-700 dark:text-brand-100">
|
||||||
<div className="flex gap-x-4">
|
<div className="flex gap-x-4">
|
||||||
<dt className="flex-none">
|
<dt className="flex-none">
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
{
|
{
|
||||||
|
"title": "Navigation",
|
||||||
"links": [
|
"links": [
|
||||||
{ "name": "Accueil", "href": "/" },
|
{
|
||||||
{ "name": "Services", "href": "/#services" },
|
"name": "Accueil",
|
||||||
{ "name": "Contact", "href": "/contact" },
|
"href": "/"
|
||||||
{ "name": "Soumission", "href": "/submission" }
|
},
|
||||||
|
{
|
||||||
|
"name": "Services",
|
||||||
|
"href": "/#services"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Contact",
|
||||||
|
"href": "/contact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Soumission",
|
||||||
|
"href": "/submission"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"cta": {
|
"cta": {
|
||||||
"label": "Soumission gratuite",
|
"label": "Soumission gratuite",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const navigationItems = nav.links.map((link) => ({ name: link.name, href: link.h
|
|||||||
navigation={navigationItems}
|
navigation={navigationItems}
|
||||||
ctaLabel={nav.cta.label}
|
ctaLabel={nav.cta.label}
|
||||||
ctaHref={nav.cta.href}
|
ctaHref={nav.cta.href}
|
||||||
client:load
|
client:only="react"
|
||||||
/>
|
/>
|
||||||
<main id="main-content">
|
<main id="main-content">
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
20
src/pages/api/auth.ts
Normal file
20
src/pages/api/auth.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { env } from 'cloudflare:workers';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
|
const clientId = (env as any).GITHUB_CLIENT_ID;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const redirectUrl = new URL('https://github.com/login/oauth/authorize');
|
||||||
|
redirectUrl.searchParams.set('client_id', clientId);
|
||||||
|
redirectUrl.searchParams.set('redirect_uri', url.origin + '/api/callback');
|
||||||
|
redirectUrl.searchParams.set('scope', 'repo user');
|
||||||
|
redirectUrl.searchParams.set('state', crypto.getRandomValues(new Uint8Array(12)).join(''));
|
||||||
|
return Response.redirect(redirectUrl.href, 302);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new Response(error.message, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
57
src/pages/api/callback.ts
Normal file
57
src/pages/api/callback.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { env } from 'cloudflare:workers';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
function renderBody(status: string, content: unknown): string {
|
||||||
|
return `<script>
|
||||||
|
const receiveMessage = (message) => {
|
||||||
|
window.opener.postMessage(
|
||||||
|
'authorization:github:${status}:${JSON.stringify(content)}',
|
||||||
|
message.origin
|
||||||
|
);
|
||||||
|
window.removeEventListener("message", receiveMessage, false);
|
||||||
|
}
|
||||||
|
window.addEventListener("message", receiveMessage, false);
|
||||||
|
window.opener.postMessage("authorizing:github", "*");
|
||||||
|
</script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
|
const clientId = (env as any).GITHUB_CLIENT_ID;
|
||||||
|
const clientSecret = (env as any).GITHUB_CLIENT_SECRET;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const code = url.searchParams.get('code');
|
||||||
|
|
||||||
|
const response = await fetch('https://github.com/login/oauth/access_token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'user-agent': 'cachetdeco-github-oauth',
|
||||||
|
'accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = (await response.json()) as any;
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return new Response(renderBody('error', result), {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'content-type': 'text/html;charset=UTF-8' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(renderBody('success', { token: result.access_token, provider: 'github' }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'text/html;charset=UTF-8' },
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return new Response(error.message, {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'content-type': 'text/html;charset=UTF-8' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
|
import { env } from 'cloudflare:workers';
|
||||||
import { sendEmail } from '@/lib/email';
|
import { sendEmail } from '@/lib/email';
|
||||||
import general from '@/content/settings/general.json';
|
import general from '@/content/settings/general.json';
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = process.env.RESEND_API_KEY;
|
const apiKey = (env as unknown as Record<string, string>).RESEND_API_KEY;
|
||||||
const toAddress = general.emailToContact?.trim();
|
const toAddress = general.emailToContact?.trim();
|
||||||
const fromAddress = general.emailFromContact?.trim();
|
const fromAddress = general.emailFromContact?.trim();
|
||||||
if (!apiKey || !toAddress || !fromAddress) {
|
if (!apiKey || !toAddress || !fromAddress) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Buffer } from 'node:buffer';
|
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
|
import { env } from 'cloudflare:workers';
|
||||||
import type { EmailAttachment } from '@/lib/email';
|
import type { EmailAttachment } from '@/lib/email';
|
||||||
import { sendEmail } from '@/lib/email';
|
import { sendEmail } from '@/lib/email';
|
||||||
import general from '@/content/settings/general.json';
|
import general from '@/content/settings/general.json';
|
||||||
@@ -206,13 +206,15 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
const safeName = sanitizeFilename(file.name);
|
const safeName = sanitizeFilename(file.name);
|
||||||
attachmentNames.push(safeName);
|
attachmentNames.push(safeName);
|
||||||
const buf = await file.arrayBuffer();
|
const buf = await file.arrayBuffer();
|
||||||
|
const bytes = new Uint8Array(buf);
|
||||||
|
const binary = bytes.reduce((acc, b) => acc + String.fromCharCode(b), '');
|
||||||
attachments.push({
|
attachments.push({
|
||||||
filename: safeName,
|
filename: safeName,
|
||||||
content: Buffer.from(buf).toString('base64'),
|
content: btoa(binary),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = process.env.RESEND_API_KEY;
|
const apiKey = (env as unknown as Record<string, string>).RESEND_API_KEY;
|
||||||
const fromAddress = general.fromAddressSubmission?.trim();
|
const fromAddress = general.fromAddressSubmission?.trim();
|
||||||
const toAddress = general.toAddressSubmission?.trim();
|
const toAddress = general.toAddressSubmission?.trim();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
},
|
},
|
||||||
"jsx": "react-jsx",
|
"types": ["@cloudflare/workers-types"],
|
||||||
"jsxImportSource": "react"
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user