wiki-viewer 1.4.2 → 1.4.3
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -27
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/agent/activity/route.js +5 -5
- package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js +6 -6
- package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js +6 -6
- package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js +6 -6
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/deny/route.js +4 -4
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/deny/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/route.js +4 -4
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js +5 -5
- package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +4 -4
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/internal/span/route.js +8 -8
- package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/settings/route.js +7 -7
- package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js +7 -7
- package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/[...all]/route.js +3 -3
- package/.next/standalone/.next/server/app/api/auth/[...all]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/auth-config/route.js +5 -5
- package/.next/standalone/.next/server/app/api/system/auth-config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/auth-settings/route.js +4 -4
- package/.next/standalone/.next/server/app/api/system/auth-settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/browse/route.js +6 -6
- package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/clear-root/route.js +5 -5
- package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/config/route.js +5 -5
- package/.next/standalone/.next/server/app/api/system/config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/pins/route.js +4 -4
- package/.next/standalone/.next/server/app/api/system/pins/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/reveal/route.js +4 -4
- package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/root-status/route.js +4 -4
- package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/set-root/route.js +5 -5
- package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/app/route.js +6 -6
- package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/content/route.js +7 -7
- package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/folder/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/move/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/new-file/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/page/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/slugs/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/watch/route.js +6 -6
- package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page.js +4 -4
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/signin/page.js +9 -4
- package/.next/standalone/.next/server/app/signin/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__03h3~3e._.js → [root-of-the-server]__04udrya._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__04g0j8i._.js → [root-of-the-server]__0kdn1le._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0d89o98._.js → src_lib_auth_server_ts_00f4e0h._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/0biy_@better-auth_core_dist_121m_lh._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/0dhv_better-auth_dist_adapters_kysely-adapter_index_mjs_08ymlcb._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0dti_kysely_dist_esm_0-t3o9q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0dti_kysely_dist_esm_01._58q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0p91_next_0f40gy0._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/{0p91_next_dist_0~2d1tl._.js → 0p91_next_dist_10ud_sa._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/0sag_@better-auth_memory-adapter_dist_index_mjs_07-_ka6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_bun-sqlite-dialect-DzNwOpKv_mjs_0lruvb8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_d1-sqlite-dialect-C2B7YsIT_mjs_03n6~tc._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_index_mjs_0gi5w.w._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_node-sqlite-dialect_mjs_0l4.y~0._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__026s8~z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06mzg2t._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0btcd4o._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0c~og-9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__134_q7l._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_051tcva._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0o~d81.._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_app_signin_00yk0vm._.js +452 -0
- package/.next/standalone/.next/server/chunks/ssr/src_app_signin_signin-form_tsx_0eznlbd._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/09-jtayuhrq~b.js +1 -0
- package/.next/standalone/.next/static/chunks/0uv7ozzpiyp2_.js +1 -0
- package/.next/standalone/.next/static/chunks/135c~rq_ux73w.js +1 -0
- package/.next/standalone/docs/agent-fs-plan.md +65 -7
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/src/app/signin/page.tsx +10 -225
- package/.next/standalone/src/app/signin/signin-form.tsx +238 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/.next/standalone/.next/server/app/signin.html +0 -1
- package/.next/standalone/.next/server/app/signin.meta +0 -15
- package/.next/standalone/.next/server/app/signin.rsc +0 -23
- package/.next/standalone/.next/server/app/signin.segments/_full.segment.rsc +0 -23
- package/.next/standalone/.next/server/app/signin.segments/_head.segment.rsc +0 -6
- package/.next/standalone/.next/server/app/signin.segments/_index.segment.rsc +0 -7
- package/.next/standalone/.next/server/app/signin.segments/_tree.segment.rsc +0 -3
- package/.next/standalone/.next/server/app/signin.segments/signin/__PAGE__.segment.rsc +0 -9
- package/.next/standalone/.next/server/app/signin.segments/signin.segment.rsc +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__088hqzy._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0c147fo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0z.1v1z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12zp2dm._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0exqvyf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_app_signin_page_tsx_1143y49._.js +0 -3
- package/.next/standalone/.next/static/chunks/0-_39e4awbk8y.js +0 -1
- package/.next/standalone/.next/static/chunks/02bxx~2cg~s6~.js +0 -1
- package/.next/standalone/.next/static/chunks/0mj_f8ncf2tks.js +0 -1
- /package/.next/standalone/.next/static/{OwXjVuoXDC_gda_BKji1J → kzOuSAQArtvD7uiAJ6ZLh}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{OwXjVuoXDC_gda_BKji1J → kzOuSAQArtvD7uiAJ6ZLh}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{OwXjVuoXDC_gda_BKji1J → kzOuSAQArtvD7uiAJ6ZLh}/_ssgManifest.js +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,419564,e=>{"use strict";var t=e.i(44501),s=e.i(101685),r=e.i(199075);e.s(["default",0,function({initialPasswordAuth:e,initialGoogle:n}){let[i,a]=(0,s.useState)("signin"),[o,l]=(0,s.useState)(""),[d,c]=(0,s.useState)(""),[u,m]=(0,s.useState)(""),[g,x]=(0,s.useState)(null),[p,f]=(0,s.useState)(!1),[h,b]=(0,s.useState)(n),[j,y]=(0,s.useState)(e),[w,v]=(0,s.useState)("/");async function N(e){e.preventDefault(),x(null),f(!0);try{if("signup"===i){let e=await r.authClient.signUp.email({email:o,password:d,name:u,callbackURL:w});e.error?x(e.error.message??"Sign-up failed"):window.location.href=w}else{let e=await r.authClient.signIn.email({email:o,password:d,callbackURL:w});if(e.error){let t=e.error.status;429===t?x("Too many sign-in attempts. Wait a minute and try again."):x(e.error.message??"Sign-in failed")}else window.location.href=w}}catch(e){x(e instanceof Error?e.message:"Unexpected error")}finally{f(!1)}}async function C(){x(null),f(!0);try{await r.authClient.signIn.social({provider:"google",callbackURL:w})}catch(e){x(e instanceof Error?e.message:"Google sign-in failed"),f(!1)}}return(0,s.useEffect)(()=>{fetch("/api/system/auth-config",{credentials:"include"}).then(e=>e.json()).then(e=>{e&&"object"==typeof e&&(b(!!e.google),y(!1!==e.passwordAuth))}).catch(()=>{});let e=new URLSearchParams(window.location.search).get("next");e&&e.startsWith("/")&&v(e)},[]),(0,t.jsx)("div",{className:"min-h-screen flex items-center justify-center bg-background",children:(0,t.jsxs)("div",{className:"w-full max-w-sm rounded-lg border border-border bg-card p-8 shadow-sm",children:[(0,t.jsx)("h1",{className:"mb-6 text-xl font-semibold text-foreground",children:"Sign in to wiki-viewer"}),h&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("button",{type:"button",onClick:C,disabled:p,className:"flex w-full items-center justify-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50",children:[(0,t.jsxs)("svg",{viewBox:"0 0 24 24",className:"h-4 w-4","aria-hidden":"true",children:[(0,t.jsx)("path",{fill:"#4285F4",d:"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"}),(0,t.jsx)("path",{fill:"#34A853",d:"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"}),(0,t.jsx)("path",{fill:"#FBBC05",d:"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"}),(0,t.jsx)("path",{fill:"#EA4335",d:"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"})]}),"Continue with Google"]}),j&&(0,t.jsxs)("div",{className:"my-4 flex items-center gap-3",children:[(0,t.jsx)("hr",{className:"flex-1 border-border"}),(0,t.jsx)("span",{className:"text-xs text-muted-foreground",children:"or"}),(0,t.jsx)("hr",{className:"flex-1 border-border"})]})]}),!j&&!h&&(0,t.jsx)("p",{className:"rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive",children:"No sign-in method is configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET, or enable email/password auth."}),j&&(0,t.jsxs)("form",{onSubmit:N,className:"space-y-4",children:["signup"===i&&(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"mb-1 block text-sm font-medium text-foreground",htmlFor:"name",children:"Name"}),(0,t.jsx)("input",{id:"name",type:"text",autoComplete:"name",value:u,onChange:e=>m(e.target.value),required:!0,className:"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"mb-1 block text-sm font-medium text-foreground",htmlFor:"email",children:"Email"}),(0,t.jsx)("input",{id:"email",type:"email",autoComplete:"email",value:o,onChange:e=>l(e.target.value),required:!0,className:"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("label",{className:"mb-1 block text-sm font-medium text-foreground",htmlFor:"password",children:"Password"}),(0,t.jsx)("input",{id:"password",type:"password",autoComplete:"signup"===i?"new-password":"current-password",value:d,onChange:e=>c(e.target.value),required:!0,minLength:8,className:"w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"})]}),g&&(0,t.jsx)("p",{className:"rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive",children:g}),(0,t.jsx)("button",{type:"submit",disabled:p,className:"w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50",children:p?"signup"===i?"Creating account...":"Signing in...":"signup"===i?"Create account":"Sign in"})]}),j&&(0,t.jsxs)("p",{className:"mt-4 text-center text-sm text-muted-foreground",children:["signin"===i?"No account?":"Already have an account?"," ",(0,t.jsx)("button",{type:"button",onClick:()=>{a("signin"===i?"signup":"signin"),x(null)},className:"font-medium text-foreground underline-offset-4 hover:underline",children:"signin"===i?"Sign up":"Sign in"})]})]})})}])}]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,824627,t=>{"use strict";var e=function(t,r){return(e=Object.setPrototypeOf||({__proto__:[]})instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])})(t,r)},r=function(){return(r=Object.assign||function(t){for(var e,r=1,n=arguments.length;r<n;r++)for(var o in e=arguments[r])Object.prototype.hasOwnProperty.call(e,o)&&(t[o]=e[o]);return t}).apply(this,arguments)};"function"==typeof SuppressedError&&SuppressedError,t.s(["__assign",()=>r,"__awaiter",0,function(t,e,r,n){return new(r||(r=Promise))(function(o,c){function s(t){try{i(n.next(t))}catch(t){c(t)}}function a(t){try{i(n.throw(t))}catch(t){c(t)}}function i(t){var e;t.done?o(t.value):((e=t.value)instanceof r?e:new r(function(t){t(e)})).then(s,a)}i((n=n.apply(t,e||[])).next())})},"__extends",0,function(t,r){if("function"!=typeof r&&null!==r)throw TypeError("Class extends value "+String(r)+" is not a constructor or null");function n(){this.constructor=t}e(t,r),t.prototype=null===r?Object.create(r):(n.prototype=r.prototype,new n)},"__generator",0,function(t,e){var r,n,o,c={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]},s=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return s.next=a(0),s.throw=a(1),s.return=a(2),"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function a(a){return function(i){var l=[a,i];if(r)throw TypeError("Generator is already executing.");for(;s&&(s=0,l[0]&&(c=0)),c;)try{if(r=1,n&&(o=2&l[0]?n.return:l[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,l[1])).done)return o;switch(n=0,o&&(l=[2&l[0],o.value]),l[0]){case 0:case 1:o=l;break;case 4:return c.label++,{value:l[1],done:!1};case 5:c.label++,n=l[1],l=[0];continue;case 7:l=c.ops.pop(),c.trys.pop();continue;default:if(!(o=(o=c.trys).length>0&&o[o.length-1])&&(6===l[0]||2===l[0])){c=0;continue}if(3===l[0]&&(!o||l[1]>o[0]&&l[1]<o[3])){c.label=l[1];break}if(6===l[0]&&c.label<o[1]){c.label=o[1],o=l;break}if(o&&c.label<o[2]){c.label=o[2],c.ops.push(l);break}o[2]&&c.ops.pop(),c.trys.pop();continue}l=e.call(t,c)}catch(t){l=[6,t],n=0}finally{r=o=0}if(5&l[0])throw l[1];return{value:l[0]?l[1]:void 0,done:!0}}}},"__rest",0,function(t,e){var r={};for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&0>e.indexOf(n)&&(r[n]=t[n]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols)for(var o=0,n=Object.getOwnPropertySymbols(t);o<n.length;o++)0>e.indexOf(n[o])&&Object.prototype.propertyIsEnumerable.call(t,n[o])&&(r[n[o]]=t[n[o]]);return r},"__spreadArray",0,function(t,e,r){if(r||2==arguments.length)for(var n,o=0,c=e.length;o<c;o++)!n&&o in e||(n||(n=Array.prototype.slice.call(e,0,o)),n[o]=e[o]);return t.concat(n||Array.prototype.slice.call(e))}])},890793,t=>{t.v(e=>Promise.all(["static/chunks/14n5cp8rr-k_b.js","static/chunks/0~fvcxnmv6g_x.js","static/chunks/1674m~gp3w-8h.js","static/chunks/0~ikzdp3hga8n.js","static/chunks/0q0.5-57agpll.js","static/chunks/0q_xpywc4mr6r.js","static/chunks/0rldt2m_ic3fr.js","static/chunks/105.j2~dvdym3.js","static/chunks/0l4qtf8hgz.11.js"].map(e=>t.l(e))).then(()=>e(229414)))},410052,t=>{t.v(e=>Promise.all(["static/chunks/0pc3jws56z~cg.js","static/chunks/144e01xuvhx~x.js","static/chunks/0vp2gi~fq70je.js"].map(e=>t.l(e))).then(()=>e(116818)))},355350,t=>{t.v(e=>Promise.all(["static/chunks/0pc3jws56z~cg.js","static/chunks/09-jtayuhrq~b.js","static/chunks/144e01xuvhx~x.js"].map(e=>t.l(e))).then(()=>e(203010)))},432302,t=>{t.v(e=>Promise.all(["static/chunks/0pc3jws56z~cg.js","static/chunks/0qvlcjvhvpaey.js"].map(e=>t.l(e))).then(()=>e(924659)))}]);
|
|
@@ -62,9 +62,10 @@ All routes reuse `checkAuth` + `enforceScope` + `withFileMutex` + `safeRootPath`
|
|
|
62
62
|
### 2.2 Write (`PUT .../file/<path>`)
|
|
63
63
|
|
|
64
64
|
- Atomic: write temp in **same dir** → `fsync` → `rename`. (`fsync` dir optional but documented.)
|
|
65
|
-
- Preserve mode of existing file. Do **not** create parent dirs unless
|
|
66
|
-
- `If-Match: <sha256>`
|
|
67
|
-
-
|
|
65
|
+
- Preserve mode of existing file. Do **not** create parent dirs unless `?mkdirs=true`.
|
|
66
|
+
- **Create = implicit.** `PUT` to a non-existent path creates the file (write-with-no-prior). Omit `If-Match` for a create; send `If-Match: <sha256>` only when overwriting. A create where the file already exists (no `If-Match`) → 412, so creates can't silently clobber.
|
|
67
|
+
- `If-Match: <sha256>` → 412 on mismatch. **Required by default for overwrites** (see R4); explicit `?force=true` bypasses and is audited.
|
|
68
|
+
- Response: `{path, sha256, size, mtime, created: bool}`.
|
|
68
69
|
|
|
69
70
|
### 2.3 Listing (`GET .../ls`)
|
|
70
71
|
|
|
@@ -82,6 +83,12 @@ All routes reuse `checkAuth` + `enforceScope` + `withFileMutex` + `safeRootPath`
|
|
|
82
83
|
- Checks: source `read`+`mutate`, dest `mutate`.
|
|
83
84
|
- For `.md`: moves the `.proof/<path>.json` sidecar too. Lock ordering: single operation lock, or lock source+dest in **sorted key order** to avoid deadlock.
|
|
84
85
|
|
|
86
|
+
### 2.6 Delete (`DELETE .../file/<path>`)
|
|
87
|
+
|
|
88
|
+
- Requires the `delete` scope op (see §3.5) and `If-Match: <sha256>` — the agent must have read the current file first, so it cannot blind-delete. This `If-Match` requirement **is** the confirmation for v1.
|
|
89
|
+
- `.md`: also deletes the sidecar (R3).
|
|
90
|
+
- Directory delete is opt-in and explicit: `?recursive=true`; without it, deleting a non-empty dir is refused. (No human approval queue in v1 — deferred.)
|
|
91
|
+
|
|
85
92
|
---
|
|
86
93
|
|
|
87
94
|
## 3. Tier 2 — Collab (unchanged)
|
|
@@ -90,14 +97,61 @@ Markdown block-ops (`block.replace/insertAfter/insertBefore/delete/append/prepen
|
|
|
90
97
|
|
|
91
98
|
---
|
|
92
99
|
|
|
100
|
+
## 3.5 Working mode vs Collaborating mode (THE central distinction)
|
|
101
|
+
|
|
102
|
+
The whole design hinges on the agent knowing **when a file is being actively co-edited by a human** (use the reviewable Tier-2 path) versus **when it's just a file to work on** (use fast raw Tier-1). Today nothing tells the agent this; a wrong guess clobbers a human's pending suggestions. We make the mode **discoverable, documented, and enforced** — three layers so the agent cannot miss it.
|
|
103
|
+
|
|
104
|
+
### The signal: `X-Collab-State` header (on every read, both tiers)
|
|
105
|
+
|
|
106
|
+
Every `GET fs/file/<path>` and every Tier-2 snapshot read returns:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
X-Collab-State: active | tracked | untracked | not-markdown
|
|
110
|
+
X-Collab-Snapshot: /api/agent/files/<path>.md # present when state != not-markdown
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| State | Meaning | Agent should… |
|
|
114
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
115
|
+
| `active` | `.md` that is **live**: either has review artifacts (pending suggestions / unresolved comments / unreverted proof-spans) **OR has a current human edit lease** (someone has the doc open in the editor). A human is collaborating right now. | **Use Tier-2 block-ops.** Your edits become reviewable suggestions. A raw `PUT` is rejected unless `If-Collab-Match` matches (see R6). |
|
|
116
|
+
| `tracked` | `.md` with a sidecar, no artifacts, no lease. | **Prefer Tier-2 for any semantic/prose edit** so provenance accrues. Raw only for mechanical/whole-file ops (e.g. reformat, regenerate). Not a free pass. |
|
|
117
|
+
| `untracked` | `.md`, no sidecar, no lease — nobody has collaborated. | Raw ok. A reviewable (Tier-2) edit creates a sidecar so future edits are tracked. |
|
|
118
|
+
| `not-markdown` | Any non-`.md` file. | **Tier-1 raw only** (Tier-2 doesn't apply). |
|
|
119
|
+
|
|
120
|
+
**Definition of `active` (two sources, OR'd):**
|
|
121
|
+
|
|
122
|
+
1. **Artifacts:** `pendingSuggestions > 0` OR `unresolvedComments > 0` OR `proofSpanCount > 0` (from sidecar — cheap, already in memory on read).
|
|
123
|
+
2. **Human edit lease:** a short-TTL presence marker (default 90s, heartbeat ~30s) set when a human opens the doc in the editor and refreshed while it stays open. **This closes the false-negative GPT-5.5 caught:** a human who opens a doc but hasn't typed a suggestion yet still reads `active`, so the agent won't blind-write into a live session. Lease lives in-memory (or the existing SQLite), keyed by `(path)`, set by a lightweight `POST /api/wiki/presence` ping the editor already can issue alongside its chokidar SSE connection. No lease + no artifacts → not active.
|
|
124
|
+
|
|
125
|
+
### Layer 2 — documented (manifest + skill)
|
|
126
|
+
|
|
127
|
+
The `/api/agents/install` manifest and the skill state the rule in one sentence:
|
|
128
|
+
|
|
129
|
+
> _"Before editing a `.md`, read it and check `X-Collab-State`. If `active`, use block-ops (Tier 2) so a human can review. Otherwise use raw fs (Tier 1). For non-markdown, always use raw fs."_
|
|
130
|
+
|
|
131
|
+
### Layer 3 — enforced (R4 + R6 backstop)
|
|
132
|
+
|
|
133
|
+
Even a confused or racing agent cannot silently clobber a live session: a raw `PUT` to an `active` `.md` is **rejected with 409 `COLLAB_ACTIVE`** unless it carries a matching `If-Collab-Match` (R6) — and the state is re-checked atomically inside the write mutex, so a session that goes active mid-flight still wins. Any genuinely orphaned suggestion/comment is marked `stale` by reconciliation (R2), never dropped. The 409 returns the Tier-2 snapshot URL, re-teaching the mode.
|
|
134
|
+
|
|
135
|
+
### What the human sees
|
|
136
|
+
|
|
137
|
+
- Agent works in Tier-2 on an `active` doc → normal reviewable suggestions (existing UX).
|
|
138
|
+
- Agent raw-writes an `active` doc anyway → `file.rawWritten by ai:<id>` in the AI Panel feed + any unbindable anchors flagged `stale` (not vanished).
|
|
139
|
+
- Agent works on `untracked`/non-md → silent, fast, audited. No collab noise.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
93
143
|
## 4. Integration rules (load-bearing)
|
|
94
144
|
|
|
95
145
|
- **R1 — Shared lock.** A raw write/move/delete on a `.md` acquires the **same** `withFileMutex` key the ops-applier uses. No interleaving with block-ops mid-proof-span.
|
|
96
146
|
- **R2 — No fake provenance.** Raw writes never author proof-spans. After a raw write to a tracked `.md`, emit `file.rawWritten` (writer known) and **reconcile the sidecar synchronously, inside the same mutex** (rebuild `refMap`, set fingerprint to `newSha`, mark affected proof refs stale). Raw edits are unmarked by definition; human sees _who_ via the event, not as an accept/revert suggestion.
|
|
97
147
|
- **GOTCHA (do not regress):** do NOT just "bump fingerprint to newSha and let existing reconciliation fire later." The existing reconciliation triggers on `sidecar.fingerprint != file sha256`; if the raw write sets fingerprint current, the mismatch is gone and rebuild never runs. Because the writer is known, reconcile **eagerly within the write**, not lazily on next read.
|
|
98
|
-
- **R3 — Sidecar lifecycle.** Raw `mv`/`rm` of a `.md` moves/deletes its sidecar
|
|
148
|
+
- **R3 — Sidecar lifecycle.** Raw `mv`/`rm` of a `.md` moves/deletes its sidecar. **NOTE:** the human `wiki/move` route currently does a bare `rename` and does NOT move the sidecar — a latent bug that orphans `.proof/` JSON today. Build a shared `moveSidecar`/`deleteSidecar` helper in `sidecar.ts` and wire **both** the raw-fs path and `wiki/move` to it (fixes the existing bug for free).
|
|
99
149
|
- **R4 — Write guard (If-Match by default).** **All** mutating raw ops (`PUT`/`DELETE`/`move`) REQUIRE `If-Match: <sha256>` by default; 412 on mismatch. An explicit `?force=true` bypasses it and is recorded in the audit row. Rationale: agents edit code/config just as much as `.md`; optional concurrency = silent lost updates, and a format-based safety boundary is arbitrary. Cheap because the agent already has the sha from `GET`.
|
|
100
|
-
- **R5 — Scope unification.** Registration `scope.paths` glob
|
|
150
|
+
- **R5 — Scope unification + `delete` op.** Registration `scope.paths` glob already covers **directories natively** (e.g. `notes/**` grants the whole subtree — no file lists). Raw-fs honors the same glob, on every surface (read/ls/search/move/delete results all re-checked). **One addition:** split `delete` out of `mutate`, so `scope.ops` becomes `[read, mutate, delete]`. This lets a human grant "edit but never delete." `mutate` = create/overwrite/move; `delete` = remove. Back-compat: existing `[read, mutate]` agents simply can't delete until re-scoped. Glob dialect (v1): `**`, `*`, `?` only — no braces/negation; advertise this in the manifest.
|
|
151
|
+
- **R6 — Collab-state precondition on raw `.md` writes (closes the TOCTOU race).** `If-Match` protects _bytes_, not _collaboration intent_: a human could create a sidecar/lease after the agent's read but before its raw `PUT`, leaving bytes unchanged so `If-Match` still passes. To prevent silently writing into a session that went `active` mid-flight:
|
|
152
|
+
- Every `.md` read also returns `X-Collab-Revision: <n>` (bumped on any sidecar/lease state change).
|
|
153
|
+
- A raw `PUT` to a `.md` must, **inside the same mutex, re-read collab state immediately before writing**: if the doc is now `active` and the request did not supply a matching `If-Collab-Match: <n>`, **reject with 409 `COLLAB_ACTIVE`** + the Tier-2 snapshot URL. The agent then switches to block-ops. `?force=true` still bypasses (audited) for deliberate overrides.
|
|
154
|
+
- This makes the mode check atomic with the write, not advisory.
|
|
101
155
|
|
|
102
156
|
### Event taxonomy (don't overload)
|
|
103
157
|
|
|
@@ -120,7 +174,8 @@ A full-file `PUT` to a `.md` can strip `<proof-span>` marks and shift block boun
|
|
|
120
174
|
Extend `/api/agents/install` manifest:
|
|
121
175
|
|
|
122
176
|
- Advertise the new `fs/*` routes.
|
|
123
|
-
- Add a `capabilities` block: `{ maxFileBytes, supportsRange, ifMatchRequired:true, forceBypass:true, search:["grep","glob"] }`.
|
|
177
|
+
- Add a `capabilities` block: `{ maxFileBytes, supportsRange, ifMatchRequired:true, forceBypass:true, search:["grep","glob"], globDialect:"**,*,?", scopeOps:["read","mutate","delete"], collabStates:["active","tracked","untracked","not-markdown"], collabPrecondition:"If-Collab-Match" }`.
|
|
178
|
+
- State the **working-vs-collaborating rule** (§3.5) in prose so the agent self-configures its tier choice.
|
|
124
179
|
- Advertise the optional MCP adapter package + version.
|
|
125
180
|
One manifest; agent self-configures. HTTP routes are the canonical contract.
|
|
126
181
|
|
|
@@ -135,6 +190,8 @@ Extend `/api/agents/install` manifest:
|
|
|
135
190
|
- `edit_file` (str-replace) → **client-side**: read → transform → `PUT If-Match`. (Server stays dumb; edit footguns live in the adapter.)
|
|
136
191
|
- `list_directory` → `GET fs/ls`
|
|
137
192
|
- `search` → `POST fs/search`
|
|
193
|
+
- `delete_file` → `DELETE fs/file` (requires `delete` scope + `If-Match`)
|
|
194
|
+
- The shim reads `X-Collab-State` and, when `active`, **warns or routes the edit through the Tier-2 block-op tools** instead of a blind raw write — so even off-the-shelf MCP clients respect collaborating mode.
|
|
138
195
|
- Do **not** make the core app MCP-native first. The app already has HTTP auth/discovery; the shim adapts to Claude Code / Cursor / Codex without poisoning the core.
|
|
139
196
|
|
|
140
197
|
---
|
|
@@ -165,7 +222,8 @@ Extend `/api/agents/install` manifest:
|
|
|
165
222
|
5. `fs/search` (grep/glob, limits).
|
|
166
223
|
6. Extend `/api/agents/install` manifest + capabilities.
|
|
167
224
|
7. `wiki-viewer-mcp` adapter (read/write/edit/list/search).
|
|
168
|
-
8.
|
|
225
|
+
8. **Mode plumbing:** human edit-lease (`POST /api/wiki/presence` + TTL, editor heartbeat), `X-Collab-State` + `X-Collab-Revision` headers on both read paths, R6 atomic re-check + 409 `COLLAB_ACTIVE` on raw `.md` writes. Plus `delete` scope op + create/`?mkdirs`/`?recursive` semantics.
|
|
226
|
+
9. Tests: traversal, symlink escape, If-Match 412, create-collision 412, sidecar move (incl. `wiki/move` bug fix), scope filtering on ls/search, big-file Range, binary skip in grep, **`X-Collab-State` matrix incl. lease-only `active`**, **R6 TOCTOU: doc goes active between read and raw PUT → 409**, stale-anchor after raw `.md` overwrite, `delete` op gating, lease expiry flips active→tracked.
|
|
169
227
|
|
|
170
228
|
```
|
|
171
229
|
|
|
@@ -1,231 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import SignInForm from "./signin-form";
|
|
2
|
+
import { passwordAuthEnabled } from "@/lib/auth/server";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
// Server component: resolve the available auth methods at render time so the
|
|
5
|
+
// initial HTML already reflects the configuration. This prevents the password
|
|
6
|
+
// form from flashing on load when it is disabled (AUTH_DISABLE_PASSWORD=1).
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
5
8
|
|
|
6
9
|
export default function SignInPage() {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const [name, setName] = useState("");
|
|
11
|
-
const [error, setError] = useState<string | null>(null);
|
|
12
|
-
const [loading, setLoading] = useState(false);
|
|
13
|
-
const [hasGoogle, setHasGoogle] = useState(false);
|
|
14
|
-
const [passwordAuth, setPasswordAuth] = useState(true);
|
|
15
|
-
const [callbackURL, setCallbackURL] = useState("/");
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
// Learn which auth methods to show. Public endpoint, no session needed.
|
|
19
|
-
fetch("/api/system/auth-config", { credentials: "include" })
|
|
20
|
-
.then((r) => r.json())
|
|
21
|
-
.then((data: unknown) => {
|
|
22
|
-
if (data && typeof data === "object") {
|
|
23
|
-
const cfg = data as { google?: boolean; passwordAuth?: boolean };
|
|
24
|
-
setHasGoogle(Boolean(cfg.google));
|
|
25
|
-
// Default to showing the password form unless told otherwise, so a
|
|
26
|
-
// failed fetch never leaves the user with no way to sign in.
|
|
27
|
-
setPasswordAuth(cfg.passwordAuth !== false);
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
.catch(() => {});
|
|
31
|
-
|
|
32
|
-
// Honour ?next= redirect param
|
|
33
|
-
const params = new URLSearchParams(window.location.search);
|
|
34
|
-
const next = params.get("next");
|
|
35
|
-
if (next && next.startsWith("/")) setCallbackURL(next);
|
|
36
|
-
}, []);
|
|
37
|
-
|
|
38
|
-
async function handleSubmit(e: FormEvent) {
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
setError(null);
|
|
41
|
-
setLoading(true);
|
|
42
|
-
try {
|
|
43
|
-
if (mode === "signup") {
|
|
44
|
-
const res = await authClient.signUp.email({
|
|
45
|
-
email,
|
|
46
|
-
password,
|
|
47
|
-
name,
|
|
48
|
-
callbackURL,
|
|
49
|
-
});
|
|
50
|
-
if (res.error) {
|
|
51
|
-
setError(res.error.message ?? "Sign-up failed");
|
|
52
|
-
} else {
|
|
53
|
-
window.location.href = callbackURL;
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
const res = await authClient.signIn.email({
|
|
57
|
-
email,
|
|
58
|
-
password,
|
|
59
|
-
callbackURL,
|
|
60
|
-
});
|
|
61
|
-
if (res.error) {
|
|
62
|
-
const status = res.error.status;
|
|
63
|
-
if (status === 429) {
|
|
64
|
-
setError("Too many sign-in attempts. Wait a minute and try again.");
|
|
65
|
-
} else {
|
|
66
|
-
setError(res.error.message ?? "Sign-in failed");
|
|
67
|
-
}
|
|
68
|
-
} else {
|
|
69
|
-
window.location.href = callbackURL;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
} catch (err: unknown) {
|
|
73
|
-
setError(err instanceof Error ? err.message : "Unexpected error");
|
|
74
|
-
} finally {
|
|
75
|
-
setLoading(false);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function handleGoogle() {
|
|
80
|
-
setError(null);
|
|
81
|
-
setLoading(true);
|
|
82
|
-
try {
|
|
83
|
-
await authClient.signIn.social({ provider: "google", callbackURL });
|
|
84
|
-
} catch (err: unknown) {
|
|
85
|
-
setError(err instanceof Error ? err.message : "Google sign-in failed");
|
|
86
|
-
setLoading(false);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
10
|
+
const google = Boolean(
|
|
11
|
+
process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET,
|
|
12
|
+
);
|
|
90
13
|
return (
|
|
91
|
-
<
|
|
92
|
-
<div className="w-full max-w-sm rounded-lg border border-border bg-card p-8 shadow-sm">
|
|
93
|
-
<h1 className="mb-6 text-xl font-semibold text-foreground">Sign in to wiki-viewer</h1>
|
|
94
|
-
|
|
95
|
-
{hasGoogle && (
|
|
96
|
-
<>
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
onClick={handleGoogle}
|
|
100
|
-
disabled={loading}
|
|
101
|
-
className="flex w-full items-center justify-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50"
|
|
102
|
-
>
|
|
103
|
-
<svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
|
|
104
|
-
<path
|
|
105
|
-
fill="#4285F4"
|
|
106
|
-
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
107
|
-
/>
|
|
108
|
-
<path
|
|
109
|
-
fill="#34A853"
|
|
110
|
-
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
111
|
-
/>
|
|
112
|
-
<path
|
|
113
|
-
fill="#FBBC05"
|
|
114
|
-
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
|
|
115
|
-
/>
|
|
116
|
-
<path
|
|
117
|
-
fill="#EA4335"
|
|
118
|
-
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
119
|
-
/>
|
|
120
|
-
</svg>
|
|
121
|
-
Continue with Google
|
|
122
|
-
</button>
|
|
123
|
-
{passwordAuth && (
|
|
124
|
-
<div className="my-4 flex items-center gap-3">
|
|
125
|
-
<hr className="flex-1 border-border" />
|
|
126
|
-
<span className="text-xs text-muted-foreground">or</span>
|
|
127
|
-
<hr className="flex-1 border-border" />
|
|
128
|
-
</div>
|
|
129
|
-
)}
|
|
130
|
-
</>
|
|
131
|
-
)}
|
|
132
|
-
|
|
133
|
-
{!passwordAuth && !hasGoogle && (
|
|
134
|
-
<p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
135
|
-
No sign-in method is configured. Set GOOGLE_CLIENT_ID and
|
|
136
|
-
GOOGLE_CLIENT_SECRET, or enable email/password auth.
|
|
137
|
-
</p>
|
|
138
|
-
)}
|
|
139
|
-
|
|
140
|
-
{passwordAuth && (
|
|
141
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
142
|
-
{mode === "signup" && (
|
|
143
|
-
<div>
|
|
144
|
-
<label className="mb-1 block text-sm font-medium text-foreground" htmlFor="name">
|
|
145
|
-
Name
|
|
146
|
-
</label>
|
|
147
|
-
<input
|
|
148
|
-
id="name"
|
|
149
|
-
type="text"
|
|
150
|
-
autoComplete="name"
|
|
151
|
-
value={name}
|
|
152
|
-
onChange={(e) => setName(e.target.value)}
|
|
153
|
-
required
|
|
154
|
-
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
155
|
-
/>
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
<div>
|
|
159
|
-
<label className="mb-1 block text-sm font-medium text-foreground" htmlFor="email">
|
|
160
|
-
Email
|
|
161
|
-
</label>
|
|
162
|
-
<input
|
|
163
|
-
id="email"
|
|
164
|
-
type="email"
|
|
165
|
-
autoComplete="email"
|
|
166
|
-
value={email}
|
|
167
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
168
|
-
required
|
|
169
|
-
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
170
|
-
/>
|
|
171
|
-
</div>
|
|
172
|
-
<div>
|
|
173
|
-
<label
|
|
174
|
-
className="mb-1 block text-sm font-medium text-foreground"
|
|
175
|
-
htmlFor="password"
|
|
176
|
-
>
|
|
177
|
-
Password
|
|
178
|
-
</label>
|
|
179
|
-
<input
|
|
180
|
-
id="password"
|
|
181
|
-
type="password"
|
|
182
|
-
autoComplete={mode === "signup" ? "new-password" : "current-password"}
|
|
183
|
-
value={password}
|
|
184
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
185
|
-
required
|
|
186
|
-
minLength={8}
|
|
187
|
-
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{error && (
|
|
192
|
-
<p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
193
|
-
{error}
|
|
194
|
-
</p>
|
|
195
|
-
)}
|
|
196
|
-
|
|
197
|
-
<button
|
|
198
|
-
type="submit"
|
|
199
|
-
disabled={loading}
|
|
200
|
-
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
201
|
-
>
|
|
202
|
-
{loading
|
|
203
|
-
? mode === "signup"
|
|
204
|
-
? "Creating account..."
|
|
205
|
-
: "Signing in..."
|
|
206
|
-
: mode === "signup"
|
|
207
|
-
? "Create account"
|
|
208
|
-
: "Sign in"}
|
|
209
|
-
</button>
|
|
210
|
-
</form>
|
|
211
|
-
)}
|
|
212
|
-
|
|
213
|
-
{passwordAuth && (
|
|
214
|
-
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
215
|
-
{mode === "signin" ? "No account?" : "Already have an account?"}{" "}
|
|
216
|
-
<button
|
|
217
|
-
type="button"
|
|
218
|
-
onClick={() => {
|
|
219
|
-
setMode(mode === "signin" ? "signup" : "signin");
|
|
220
|
-
setError(null);
|
|
221
|
-
}}
|
|
222
|
-
className="font-medium text-foreground underline-offset-4 hover:underline"
|
|
223
|
-
>
|
|
224
|
-
{mode === "signin" ? "Sign up" : "Sign in"}
|
|
225
|
-
</button>
|
|
226
|
-
</p>
|
|
227
|
-
)}
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
14
|
+
<SignInForm initialPasswordAuth={passwordAuthEnabled} initialGoogle={google} />
|
|
230
15
|
);
|
|
231
16
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, type FormEvent } from "react";
|
|
4
|
+
import { authClient } from "@/lib/auth/client";
|
|
5
|
+
|
|
6
|
+
export default function SignInForm({
|
|
7
|
+
initialPasswordAuth,
|
|
8
|
+
initialGoogle,
|
|
9
|
+
}: {
|
|
10
|
+
initialPasswordAuth: boolean;
|
|
11
|
+
initialGoogle: boolean;
|
|
12
|
+
}) {
|
|
13
|
+
const [mode, setMode] = useState<"signin" | "signup">("signin");
|
|
14
|
+
const [email, setEmail] = useState("");
|
|
15
|
+
const [password, setPassword] = useState("");
|
|
16
|
+
const [name, setName] = useState("");
|
|
17
|
+
const [error, setError] = useState<string | null>(null);
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
// Seed from server-rendered values so the first paint already matches the
|
|
20
|
+
// configured auth methods. This avoids a flash of the password form when it
|
|
21
|
+
// is disabled. The fetch below only refreshes in case config changed.
|
|
22
|
+
const [hasGoogle, setHasGoogle] = useState(initialGoogle);
|
|
23
|
+
const [passwordAuth, setPasswordAuth] = useState(initialPasswordAuth);
|
|
24
|
+
const [callbackURL, setCallbackURL] = useState("/");
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
// Refresh which auth methods to show. Public endpoint, no session needed.
|
|
28
|
+
fetch("/api/system/auth-config", { credentials: "include" })
|
|
29
|
+
.then((r) => r.json())
|
|
30
|
+
.then((data: unknown) => {
|
|
31
|
+
if (data && typeof data === "object") {
|
|
32
|
+
const cfg = data as { google?: boolean; passwordAuth?: boolean };
|
|
33
|
+
setHasGoogle(Boolean(cfg.google));
|
|
34
|
+
setPasswordAuth(cfg.passwordAuth !== false);
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.catch(() => {});
|
|
38
|
+
|
|
39
|
+
// Honour ?next= redirect param
|
|
40
|
+
const params = new URLSearchParams(window.location.search);
|
|
41
|
+
const next = params.get("next");
|
|
42
|
+
if (next && next.startsWith("/")) setCallbackURL(next);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
async function handleSubmit(e: FormEvent) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
setError(null);
|
|
48
|
+
setLoading(true);
|
|
49
|
+
try {
|
|
50
|
+
if (mode === "signup") {
|
|
51
|
+
const res = await authClient.signUp.email({
|
|
52
|
+
email,
|
|
53
|
+
password,
|
|
54
|
+
name,
|
|
55
|
+
callbackURL,
|
|
56
|
+
});
|
|
57
|
+
if (res.error) {
|
|
58
|
+
setError(res.error.message ?? "Sign-up failed");
|
|
59
|
+
} else {
|
|
60
|
+
window.location.href = callbackURL;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
const res = await authClient.signIn.email({
|
|
64
|
+
email,
|
|
65
|
+
password,
|
|
66
|
+
callbackURL,
|
|
67
|
+
});
|
|
68
|
+
if (res.error) {
|
|
69
|
+
const status = res.error.status;
|
|
70
|
+
if (status === 429) {
|
|
71
|
+
setError("Too many sign-in attempts. Wait a minute and try again.");
|
|
72
|
+
} else {
|
|
73
|
+
setError(res.error.message ?? "Sign-in failed");
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
window.location.href = callbackURL;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
setError(err instanceof Error ? err.message : "Unexpected error");
|
|
81
|
+
} finally {
|
|
82
|
+
setLoading(false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleGoogle() {
|
|
87
|
+
setError(null);
|
|
88
|
+
setLoading(true);
|
|
89
|
+
try {
|
|
90
|
+
await authClient.signIn.social({ provider: "google", callbackURL });
|
|
91
|
+
} catch (err: unknown) {
|
|
92
|
+
setError(err instanceof Error ? err.message : "Google sign-in failed");
|
|
93
|
+
setLoading(false);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
99
|
+
<div className="w-full max-w-sm rounded-lg border border-border bg-card p-8 shadow-sm">
|
|
100
|
+
<h1 className="mb-6 text-xl font-semibold text-foreground">Sign in to wiki-viewer</h1>
|
|
101
|
+
|
|
102
|
+
{hasGoogle && (
|
|
103
|
+
<>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={handleGoogle}
|
|
107
|
+
disabled={loading}
|
|
108
|
+
className="flex w-full items-center justify-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50"
|
|
109
|
+
>
|
|
110
|
+
<svg viewBox="0 0 24 24" className="h-4 w-4" aria-hidden="true">
|
|
111
|
+
<path
|
|
112
|
+
fill="#4285F4"
|
|
113
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
114
|
+
/>
|
|
115
|
+
<path
|
|
116
|
+
fill="#34A853"
|
|
117
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
118
|
+
/>
|
|
119
|
+
<path
|
|
120
|
+
fill="#FBBC05"
|
|
121
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
|
|
122
|
+
/>
|
|
123
|
+
<path
|
|
124
|
+
fill="#EA4335"
|
|
125
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
126
|
+
/>
|
|
127
|
+
</svg>
|
|
128
|
+
Continue with Google
|
|
129
|
+
</button>
|
|
130
|
+
{passwordAuth && (
|
|
131
|
+
<div className="my-4 flex items-center gap-3">
|
|
132
|
+
<hr className="flex-1 border-border" />
|
|
133
|
+
<span className="text-xs text-muted-foreground">or</span>
|
|
134
|
+
<hr className="flex-1 border-border" />
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{!passwordAuth && !hasGoogle && (
|
|
141
|
+
<p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
142
|
+
No sign-in method is configured. Set GOOGLE_CLIENT_ID and
|
|
143
|
+
GOOGLE_CLIENT_SECRET, or enable email/password auth.
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{passwordAuth && (
|
|
148
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
149
|
+
{mode === "signup" && (
|
|
150
|
+
<div>
|
|
151
|
+
<label className="mb-1 block text-sm font-medium text-foreground" htmlFor="name">
|
|
152
|
+
Name
|
|
153
|
+
</label>
|
|
154
|
+
<input
|
|
155
|
+
id="name"
|
|
156
|
+
type="text"
|
|
157
|
+
autoComplete="name"
|
|
158
|
+
value={name}
|
|
159
|
+
onChange={(e) => setName(e.target.value)}
|
|
160
|
+
required
|
|
161
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
<div>
|
|
166
|
+
<label className="mb-1 block text-sm font-medium text-foreground" htmlFor="email">
|
|
167
|
+
Email
|
|
168
|
+
</label>
|
|
169
|
+
<input
|
|
170
|
+
id="email"
|
|
171
|
+
type="email"
|
|
172
|
+
autoComplete="email"
|
|
173
|
+
value={email}
|
|
174
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
175
|
+
required
|
|
176
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
<div>
|
|
180
|
+
<label
|
|
181
|
+
className="mb-1 block text-sm font-medium text-foreground"
|
|
182
|
+
htmlFor="password"
|
|
183
|
+
>
|
|
184
|
+
Password
|
|
185
|
+
</label>
|
|
186
|
+
<input
|
|
187
|
+
id="password"
|
|
188
|
+
type="password"
|
|
189
|
+
autoComplete={mode === "signup" ? "new-password" : "current-password"}
|
|
190
|
+
value={password}
|
|
191
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
192
|
+
required
|
|
193
|
+
minLength={8}
|
|
194
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{error && (
|
|
199
|
+
<p className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
200
|
+
{error}
|
|
201
|
+
</p>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
<button
|
|
205
|
+
type="submit"
|
|
206
|
+
disabled={loading}
|
|
207
|
+
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
208
|
+
>
|
|
209
|
+
{loading
|
|
210
|
+
? mode === "signup"
|
|
211
|
+
? "Creating account..."
|
|
212
|
+
: "Signing in..."
|
|
213
|
+
: mode === "signup"
|
|
214
|
+
? "Create account"
|
|
215
|
+
: "Sign in"}
|
|
216
|
+
</button>
|
|
217
|
+
</form>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{passwordAuth && (
|
|
221
|
+
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
222
|
+
{mode === "signin" ? "No account?" : "Already have an account?"}{" "}
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
onClick={() => {
|
|
226
|
+
setMode(mode === "signin" ? "signup" : "signin");
|
|
227
|
+
setError(null);
|
|
228
|
+
}}
|
|
229
|
+
className="font-medium text-foreground underline-offset-4 hover:underline"
|
|
230
|
+
>
|
|
231
|
+
{mode === "signin" ? "Sign up" : "Sign in"}
|
|
232
|
+
</button>
|
|
233
|
+
</p>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|