moonhead 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -30,3 +30,43 @@ import { db } from "moonhead/db";
30
30
  import { users } from "moonhead/db/schema";
31
31
  import { requireSession } from "moonhead/feature/auth/authorization";
32
32
  ```
33
+
34
+ ## External affiliate authentication
35
+
36
+ Set the dashboard environment variables:
37
+
38
+ ```dotenv
39
+ APP_URL="https://dashboard.example.com"
40
+ AFFILIATE_SITE_URL="https://example.com"
41
+ ```
42
+
43
+ The website must call the dashboard API directly from the browser with
44
+ `credentials: "include"`. The API accepts only the exact
45
+ `AFFILIATE_SITE_URL` origin and sets host-only, HttpOnly session cookies for
46
+ the dashboard.
47
+
48
+ ```ts
49
+ const response = await fetch(
50
+ "https://dashboard.example.com/api/affiliate/auth/login",
51
+ {
52
+ method: "POST",
53
+ credentials: "include",
54
+ headers: { "content-type": "application/json" },
55
+ body: JSON.stringify({ email, password, totpCode }),
56
+ },
57
+ );
58
+ ```
59
+
60
+ Available endpoints:
61
+
62
+ - `GET /api/affiliate/auth/config`
63
+ - `POST /api/affiliate/auth/login` with `email`, `password`, and optional
64
+ `totpCode`
65
+ - `POST /api/affiliate/auth/signup` with `email`, `firstName`, `lastName`,
66
+ `password`, `confirmPassword`, and `tosAccepted: true`
67
+ - `POST /api/affiliate/auth/verify-email` with `token`
68
+
69
+ Every request, including `GET`, must send `credentials: "include"`. Login
70
+ returns `totpRequired: true` when a second request with the TOTP code is
71
+ needed. Successful login and automatic activation return the dashboard URL in
72
+ `redirectTo`.
@@ -27,7 +27,5 @@ export declare const changePasswordAction: (args_0: FormState<{
27
27
  }>>;
28
28
  export type ResetPasswordState = FormState;
29
29
  export declare const resetPasswordAction: (args_0: string, args_1: FormState<void>, args_2: FormData) => Promise<FormState<void>>;
30
- export type SignupState = FormState;
31
- export declare const signupAction: (args_0: FormState<never>, args_1: FormData) => Promise<FormState<never>>;
32
30
  export type VerifyEmailState = FormState;
33
31
  export declare const verifyEmailAction: (args_0: string, args_1: FormState<void>, args_2: FormData) => Promise<FormState<void>>;
@@ -1 +1 @@
1
- "use server";import{db as d}from"../../db/index.js";import{canSignIn as h,SIGNUP_SOURCE as I,USER_ROLE as N,users as t}from"../../db/schema/index.js";import{and as j,eq as m}from"drizzle-orm";import{redirect as f}from"next/navigation";import{z as r}from"zod";import{notifyManagersOfActivatedAffiliate as b,notifyManagersOfPendingAffiliate as C}from"../affiliates/pending-notification.js";import{sendPasswordResetEmail as O}from"./password-reset.js";import{sendSignupVerificationEmail as V}from"./signup-email.js";import{parseForm as v}from"../../lib/actions/parse-form.js";import{authFormAction as D,formAction as u}from"../../lib/auth/action.js";import{hashPassword as P,verifyPassword as M}from"../../lib/auth/password.js";import{signIn as S,signOut as L}from"../../lib/auth/session.js";import{revokeAllSessionsForUser as T}from"../../lib/auth/tokens.js";import{decryptSecret as U,verifyCode as W}from"../../lib/auth/totp.js";import{clearTotpChallenge as x,issueTotpChallenge as X,readTotpChallenge as z}from"../../lib/auth/totp-challenge.js";import{consumeVerificationToken as k,inspectVerificationToken as G}from"../../lib/auth/verification-tokens.js";import{isUniqueViolation as $}from"../../lib/db/errors.js";import{appError as o}from"../../lib/errors/codes.js";import{success as R}from"../../lib/errors/server.js";import{getPlatformSettings as E}from"../../lib/platform-settings.js";import{hydratePreferencesToCookies as A}from"../../lib/preferences/index.js";import{formDataToObject as q,NAME_MAX as B,PASSWORD_MAX as y,PASSWORD_MIN as _,UserProfileSchema as J,V as g}from"../../lib/validation/zod.js";const K=r.object({email:r.email().trim().toLowerCase(),password:r.string().min(_).max(y)}),It=u({},async a=>{const n=K.safeParse(q(a));if(!n.success)throw o("invalid_credentials",{field:"password"});const[i]=await d.select().from(t).where(m(t.email,n.data.email)).limit(1);if(!i?.passwordHash)throw o("invalid_credentials",{field:"password"});if(!await M(i.passwordHash,n.data.password))throw o("invalid_credentials",{field:"password"});if(!h(i.status))throw o("user_blocked",{toast:!0});if(!i.inviteAcceptedAt)throw o("email_not_verified",{toast:!0});if(i.role===N.affiliate&&!i.approvedAt)throw o("affiliate_not_approved",{toast:!0});if(i.totpConfirmedAt)return await X(i.id),{totpRequired:!0};await S(i.id),await A(i.id),(await E()).requireManagerTotp&&i.role===N.manager&&f("/user/settings"),f("/")}),Q=r.object({code:r.string().regex(/^\d{6}$/)}),kt=u({},async a=>{const n=await v(a,Q),i=await z();if(!i)throw o("totp_challenge_expired");const[e]=await d.select({id:t.id,email:t.email,status:t.status,totpSecret:t.totpSecret,totpConfirmedAt:t.totpConfirmedAt}).from(t).where(m(t.id,i)).limit(1);if(!e?.totpSecret||!e.totpConfirmedAt)throw await x(),o("totp_challenge_expired");if(!h(e.status))throw await x(),o("user_blocked",{toast:!0});const s=U(e.totpSecret);if(!W(s,n.code,e.email))throw o("totp_invalid_code",{field:"code"});await x(),await S(e.id),await A(e.id),f("/")}),Et=async()=>{await x()};async function Ft(){await L(),f("/signin")}const Y=r.object({password:r.string().min(_).max(y),confirmPassword:r.string()}).refine(a=>a.password===a.confirmPassword,{path:["confirmPassword"],message:g.passwordsMustMatch}),bt=u({},async(a,n)=>{const i=await v(n,Y),e=await k(a,"invite");if(e.kind!=="ok")throw o("invalid_token");const[s]=await d.select({signupSource:t.signupSource,role:t.role,email:t.email,firstName:t.firstName,lastName:t.lastName,status:t.status}).from(t).where(m(t.id,e.userId)).limit(1);if(!s)throw o("user_not_found",{toast:!0});if(!h(s.status))throw o("user_blocked",{toast:!0});const p=await P(i.password),l=new Date,c=s.signupSource===I.affiliate_invite,H=await E(),F=c&&H.requireAffiliateApproval;if(await d.update(t).set({passwordHash:p,inviteAcceptedAt:l,...F?{}:{approvedAt:l}}).where(m(t.id,e.userId)),F){try{await C({affiliateEmail:s.email,affiliateFirstName:s.firstName,affiliateLastName:s.lastName})}catch(w){console.error("[acceptInvite] pending notification failed",{error:w instanceof Error?w.message:String(w),userId:e.userId}),f("/signup/awaiting-approval?notify=failed")}f("/signup/awaiting-approval")}if(s.role===N.affiliate)try{await b({affiliateEmail:s.email,affiliateFirstName:s.firstName,affiliateLastName:s.lastName})}catch(w){console.error("[acceptInvite] manager notification failed",{error:w instanceof Error?w.message:String(w),userId:e.userId})}await S(e.userId),await A(e.userId),f("/")}),Z=r.object({email:r.email().trim().toLowerCase()}),Ct=u({},async a=>{const n=Z.safeParse(q(a));if(!n.success)return;const[i]=await d.select({id:t.id,email:t.email,firstName:t.firstName,status:t.status}).from(t).where(m(t.email,n.data.email)).limit(1);if(!(!i||!h(i.status)))try{await O({userId:i.id,email:i.email,firstName:i.firstName})}catch{return R("Toasts.passwordResetEmailFailed")}}),tt=r.object({password:r.string().min(_).max(y),confirmPassword:r.string()}).refine(a=>a.password===a.confirmPassword,{path:["confirmPassword"],message:g.passwordsMustMatch}),at=r.object({currentPassword:r.string().min(1,g.fieldRequired),password:r.string().min(_,g.passwordTooShort).max(y),confirmPassword:r.string()}).refine(a=>a.password===a.confirmPassword,{path:["confirmPassword"],message:g.passwordsMustMatch}),Mt=D({},async({user:a,audit:n},i)=>{const e=await v(i,at),[s]=await d.select({passwordHash:t.passwordHash}).from(t).where(m(t.id,a.id)).limit(1);if(!s?.passwordHash)throw o("invalid_credentials",{field:"currentPassword"});if(!await M(s.passwordHash,e.currentPassword))throw o("invalid_credentials",{field:"currentPassword"});const l=await P(e.password);return await d.transaction(async c=>{await c.update(t).set({passwordHash:l}).where(m(t.id,a.id)),await n({action:"user.password.change",subjectId:a.id,target:{type:"user",id:a.id,label:a.email}},c)}),await T(a.id),await S(a.id),await R("Toasts.passwordChanged")}),Tt=u({},async(a,n)=>{const i=await v(n,tt),e=await k(a,"password_reset");if(e.kind!=="ok")throw o("invalid_token");const[s]=await d.select({id:t.id,status:t.status}).from(t).where(m(t.id,e.userId)).limit(1);if(!s)throw o("user_not_found",{toast:!0});if(!h(s.status))throw o("user_blocked",{toast:!0});const p=await P(i.password);await d.update(t).set({passwordHash:p}).where(m(t.id,s.id)),await T(s.id),await S(s.id),await A(s.id),f("/")}),et=J.extend({password:r.string().min(_).max(y),confirmPassword:r.string().max(B),tosAccepted:r.preprocess(a=>a==="on"||a==="true"||a===!0,r.literal(!0,g.tosRequired))}).refine(a=>a.password===a.confirmPassword,{path:["confirmPassword"],message:g.passwordsMustMatch}),Rt=u({},async a=>{const n=await v(a,et),i=await P(n.password),e=new Date,[s]=await d.insert(t).values({email:n.email,firstName:n.firstName,lastName:n.lastName,passwordHash:i,role:N.affiliate,signupSource:I.self_signup,inviteAcceptedAt:null,tosAcceptedAt:e}).returning({id:t.id,email:t.email,firstName:t.firstName}).catch(p=>{if($(p))return[];throw p});if(!s)throw o("email_already_exists",{field:"email"});try{await V({userId:s.id,email:s.email,firstName:s.firstName})}catch{}f("/signup/check-email")}),it=r.object({token:r.string().min(1)}),qt=u({},async(a,n)=>{it.parse({token:a});const i=await G(a,"email_verify");if(i.kind!=="ok")throw o("invalid_token");const[e]=await d.select({id:t.id,email:t.email,firstName:t.firstName,lastName:t.lastName,status:t.status}).from(t).where(j(m(t.id,i.userId),m(t.signupSource,I.self_signup))).limit(1);if(!e)throw o("invalid_token");if(!h(e.status))throw o("user_blocked",{toast:!0});if((await k(a,"email_verify")).kind!=="ok")throw o("invalid_token");const p=await E(),l=new Date;await d.update(t).set({inviteAcceptedAt:l,...p.requireAffiliateApproval?{}:{approvedAt:l}}).where(m(t.id,e.id)),p.requireAffiliateApproval&&f(await C({affiliateEmail:e.email,affiliateFirstName:e.firstName,affiliateLastName:e.lastName}).then(()=>"/signup/awaiting-approval").catch(c=>(console.error("[verifyEmail] manager notification failed",{error:c instanceof Error?c.message:String(c),userId:e.id}),"/signup/awaiting-approval?notify=failed")));try{await b({affiliateEmail:e.email,affiliateFirstName:e.firstName,affiliateLastName:e.lastName})}catch(c){console.error("[verifyEmail] manager notification failed",{error:c instanceof Error?c.message:String(c),userId:e.id})}await S(e.id),await A(e.id),f("/")});export{bt as acceptInviteAction,Et as cancelTotpChallengeAction,Mt as changePasswordAction,Ct as forgotPasswordAction,Tt as resetPasswordAction,It as signInAction,Ft as signOutAction,Rt as signupAction,qt as verifyEmailAction,kt as verifyTotpAction};
1
+ "use server";import{and as q,eq as c}from"drizzle-orm";import{redirect as f}from"next/navigation";import{z as o}from"zod";import{db as m}from"../../db/index.js";import{canSignIn as u,SIGNUP_SOURCE as b,USER_ROLE as N,users as t}from"../../db/schema/index.js";import{parseForm as y}from"../../lib/actions/parse-form.js";import{authFormAction as O,formAction as h}from"../../lib/auth/action.js";import{hashPassword as I,verifyPassword as F}from"../../lib/auth/password.js";import{signIn as g,signOut as L}from"../../lib/auth/session.js";import{revokeAllSessionsForUser as C}from"../../lib/auth/tokens.js";import{decryptSecret as V,verifyCode as D}from"../../lib/auth/totp.js";import{clearTotpChallenge as A,issueTotpChallenge as U,readTotpChallenge as W}from"../../lib/auth/totp-challenge.js";import{consumeVerificationToken as k,inspectVerificationToken as z}from"../../lib/auth/verification-tokens.js";import{appError as s}from"../../lib/errors/codes.js";import{success as T}from"../../lib/errors/server.js";import{getPlatformSettings as x}from"../../lib/platform-settings.js";import{hydratePreferencesToCookies as v}from"../../lib/preferences/index.js";import{formDataToObject as R,PASSWORD_MAX as _,PASSWORD_MIN as P,V as S}from"../../lib/validation/zod.js";import{notifyManagersOfActivatedAffiliate as M,notifyManagersOfPendingAffiliate as j}from"../affiliates/pending-notification.js";import{sendPasswordResetEmail as G}from"./password-reset.js";const X=o.object({email:o.email().trim().toLowerCase(),password:o.string().min(P).max(_)}),St=h({},async e=>{const n=X.safeParse(R(e));if(!n.success)throw s("invalid_credentials",{field:"password"});const[i]=await m.select().from(t).where(c(t.email,n.data.email)).limit(1);if(!i?.passwordHash)throw s("invalid_credentials",{field:"password"});if(!await F(i.passwordHash,n.data.password))throw s("invalid_credentials",{field:"password"});if(i.role===N.affiliate)throw s("invalid_credentials",{field:"password"});if(!u(i.status))throw s("user_blocked",{toast:!0});if(!i.inviteAcceptedAt)throw s("email_not_verified",{toast:!0});if(i.totpConfirmedAt)return await U(i.id),{totpRequired:!0};await g(i.id),await v(i.id),(await x()).requireManagerTotp&&i.role===N.manager&&f("/user/settings"),f("/")}),$=o.object({code:o.string().regex(/^\d{6}$/)}),yt=h({},async e=>{const n=await y(e,$),i=await W();if(!i)throw s("totp_challenge_expired");const[a]=await m.select({id:t.id,email:t.email,status:t.status,totpSecret:t.totpSecret,totpConfirmedAt:t.totpConfirmedAt}).from(t).where(c(t.id,i)).limit(1);if(!a?.totpSecret||!a.totpConfirmedAt)throw await A(),s("totp_challenge_expired");if(!u(a.status))throw await A(),s("user_blocked",{toast:!0});const r=V(a.totpSecret);if(!D(r,n.code,a.email))throw s("totp_invalid_code",{field:"code"});await A(),await g(a.id),await v(a.id),f("/")}),At=async()=>{await A()};async function _t(){await L(),f("/signin")}const B=o.object({password:o.string().min(P).max(_),confirmPassword:o.string()}).refine(e=>e.password===e.confirmPassword,{path:["confirmPassword"],message:S.passwordsMustMatch}),Pt=h({},async(e,n)=>{const i=await y(n,B),a=await k(e,"invite");if(a.kind!=="ok")throw s("invalid_token");const[r]=await m.select({signupSource:t.signupSource,role:t.role,email:t.email,firstName:t.firstName,lastName:t.lastName,status:t.status}).from(t).where(c(t.id,a.userId)).limit(1);if(!r)throw s("user_not_found",{toast:!0});if(!u(r.status))throw s("user_blocked",{toast:!0});const l=await I(i.password),p=new Date,d=r.signupSource===b.affiliate_invite,H=await x(),E=d&&H.requireAffiliateApproval;if(await m.update(t).set({passwordHash:l,inviteAcceptedAt:p,...E?{}:{approvedAt:p}}).where(c(t.id,a.userId)),E){try{await j({affiliateEmail:r.email,affiliateFirstName:r.firstName,affiliateLastName:r.lastName})}catch(w){console.error("[acceptInvite] pending notification failed",{error:w instanceof Error?w.message:String(w),userId:a.userId}),f("/signup/awaiting-approval?notify=failed")}f("/signup/awaiting-approval")}if(r.role===N.affiliate)try{await M({affiliateEmail:r.email,affiliateFirstName:r.firstName,affiliateLastName:r.lastName})}catch(w){console.error("[acceptInvite] manager notification failed",{error:w instanceof Error?w.message:String(w),userId:a.userId})}await g(a.userId),await v(a.userId),f("/")}),J=o.object({email:o.email().trim().toLowerCase()}),Nt=h({},async e=>{const n=J.safeParse(R(e));if(!n.success)return;const[i]=await m.select({id:t.id,email:t.email,firstName:t.firstName,status:t.status}).from(t).where(c(t.email,n.data.email)).limit(1);if(!(!i||!u(i.status)))try{await G({userId:i.id,email:i.email,firstName:i.firstName})}catch{return T("Toasts.passwordResetEmailFailed")}}),K=o.object({password:o.string().min(P).max(_),confirmPassword:o.string()}).refine(e=>e.password===e.confirmPassword,{path:["confirmPassword"],message:S.passwordsMustMatch}),Q=o.object({currentPassword:o.string().min(1,S.fieldRequired),password:o.string().min(P,S.passwordTooShort).max(_),confirmPassword:o.string()}).refine(e=>e.password===e.confirmPassword,{path:["confirmPassword"],message:S.passwordsMustMatch}),It=O({},async({user:e,audit:n},i)=>{const a=await y(i,Q),[r]=await m.select({passwordHash:t.passwordHash}).from(t).where(c(t.id,e.id)).limit(1);if(!r?.passwordHash)throw s("invalid_credentials",{field:"currentPassword"});if(!await F(r.passwordHash,a.currentPassword))throw s("invalid_credentials",{field:"currentPassword"});const p=await I(a.password);return await m.transaction(async d=>{await d.update(t).set({passwordHash:p}).where(c(t.id,e.id)),await n({action:"user.password.change",subjectId:e.id,target:{type:"user",id:e.id,label:e.email}},d)}),await C(e.id),await g(e.id),await T("Toasts.passwordChanged")}),kt=h({},async(e,n)=>{const i=await y(n,K),a=await k(e,"password_reset");if(a.kind!=="ok")throw s("invalid_token");const[r]=await m.select({id:t.id,status:t.status}).from(t).where(c(t.id,a.userId)).limit(1);if(!r)throw s("user_not_found",{toast:!0});if(!u(r.status))throw s("user_blocked",{toast:!0});const l=await I(i.password);await m.update(t).set({passwordHash:l}).where(c(t.id,r.id)),await C(r.id),await g(r.id),await v(r.id),f("/")}),Y=o.object({token:o.string().min(1)}),xt=h({},async(e,n)=>{Y.parse({token:e});const i=await z(e,"email_verify");if(i.kind!=="ok")throw s("invalid_token");const[a]=await m.select({id:t.id,email:t.email,firstName:t.firstName,lastName:t.lastName,status:t.status}).from(t).where(q(c(t.id,i.userId),c(t.signupSource,b.self_signup))).limit(1);if(!a)throw s("invalid_token");if(!u(a.status))throw s("user_blocked",{toast:!0});if((await k(e,"email_verify")).kind!=="ok")throw s("invalid_token");const l=await x(),p=new Date;await m.update(t).set({inviteAcceptedAt:p,...l.requireAffiliateApproval?{}:{approvedAt:p}}).where(c(t.id,a.id)),l.requireAffiliateApproval&&f(await j({affiliateEmail:a.email,affiliateFirstName:a.firstName,affiliateLastName:a.lastName}).then(()=>"/signup/awaiting-approval").catch(d=>(console.error("[verifyEmail] manager notification failed",{error:d instanceof Error?d.message:String(d),userId:a.id}),"/signup/awaiting-approval?notify=failed")));try{await M({affiliateEmail:a.email,affiliateFirstName:a.firstName,affiliateLastName:a.lastName})}catch(d){console.error("[verifyEmail] manager notification failed",{error:d instanceof Error?d.message:String(d),userId:a.id})}await g(a.id),await v(a.id),f("/")});export{Pt as acceptInviteAction,At as cancelTotpChallengeAction,It as changePasswordAction,Nt as forgotPasswordAction,kt as resetPasswordAction,St as signInAction,_t as signOutAction,xt as verifyEmailAction,yt as verifyTotpAction};
@@ -0,0 +1,6 @@
1
+ import "server-only";
2
+ export declare function affiliateAuthOptions(request: Request): Response;
3
+ export declare function affiliateAuthConfig(request: Request): Response;
4
+ export declare function affiliateLogin(request: Request): Promise<Response>;
5
+ export declare function affiliateSignup(request: Request): Promise<Response>;
6
+ export declare function affiliateVerifyEmail(request: Request): Promise<Response>;
@@ -0,0 +1 @@
1
+ import"server-only";import{and as N,eq as m}from"drizzle-orm";import{z as s}from"zod";import{db as p}from"../../db/index.js";import{canSignIn as A,SIGNUP_SOURCE as h,USER_ROLE as R,users as n}from"../../db/schema/index.js";import{hashPassword as y,verifyPassword as L}from"../../lib/auth/password.js";import{signIn as v}from"../../lib/auth/session.js";import{decryptSecret as I,verifyCode as C}from"../../lib/auth/totp.js";import{consumeVerificationToken as U,inspectVerificationToken as x}from"../../lib/auth/verification-tokens.js";import{isUniqueViolation as T}from"../../lib/db/errors.js";import{env as d}from"../../lib/env.js";import{getPlatformSettings as O}from"../../lib/platform-settings.js";import{hydratePreferencesToCookies as S}from"../../lib/preferences/index.js";import{NAME_MAX as q,PASSWORD_MAX as k,PASSWORD_MIN as E,UserProfileSchema as b}from"../../lib/validation/zod.js";import{notifyManagersOfActivatedAffiliate as V,notifyManagersOfPendingAffiliate as F}from"../affiliates/pending-notification.js";import{sendSignupVerificationEmail as M}from"./signup-email.js";const H=s.object({email:s.email().trim().toLowerCase(),password:s.string().min(E).max(k),totpCode:s.string().regex(/^\d{6}$/).optional()}),j=b.extend({password:s.string().min(E).max(k),confirmPassword:s.string().max(q),tosAccepted:s.literal(!0)}).refine(i=>i.password===i.confirmPassword,{path:["confirmPassword"]}),D=s.object({token:s.string().min(1).max(512)});function G(){if(d.AFFILIATE_SITE_URL)return new URL(d.AFFILIATE_SITE_URL);if(d.NODE_ENV!=="production")return new URL(d.APP_URL);throw new Error("AFFILIATE_SITE_URL is required in production")}function P(i){return{"Access-Control-Allow-Credentials":"true","Access-Control-Allow-Headers":"Content-Type","Access-Control-Allow-Methods":"GET, POST, OPTIONS","Access-Control-Allow-Origin":i,"Cache-Control":"no-store",Vary:"Origin"}}function u(i){const e=i.headers.get("origin");return e===G().origin?e:null}function f(i,e,r=200){return Response.json(e,{status:r,headers:P(i)})}function o(i,e,r){return f(i,{ok:!1,error:{code:e}},r)}async function w(i){if(Number(i.headers.get("content-length")??"0")>16384)throw new Error("request_too_large");return i.json()}function oe(i){const e=u(i);return e?new Response(null,{status:204,headers:P(e)}):new Response(null,{status:403})}function se(i){const e=u(i);return e?f(e,{ok:!0,dashboardUrl:d.APP_URL}):new Response(null,{status:403})}async function fe(i){const e=u(i);if(!e)return new Response(null,{status:403});const r=H.safeParse(await w(i).catch(()=>null));if(!r.success)return o(e,"invalid_input",400);const[a]=await p.select().from(n).where(m(n.email,r.data.email)).limit(1);if(!a?.passwordHash||a.role!==R.affiliate||!await L(a.passwordHash,r.data.password))return o(e,"invalid_credentials",401);if(!A(a.status))return o(e,"user_blocked",403);if(!a.inviteAcceptedAt)return o(e,"email_not_verified",403);if(!a.approvedAt)return o(e,"affiliate_not_approved",403);if(a.totpConfirmedAt){if(!r.data.totpCode)return f(e,{ok:!1,totpRequired:!0});if(!a.totpSecret)return o(e,"user_blocked",403);const t=I(a.totpSecret);if(!C(t,r.data.totpCode,a.email))return o(e,"totp_invalid_code",401)}return await v(a.id),await S(a.id),f(e,{ok:!0,redirectTo:d.APP_URL})}async function le(i){const e=u(i);if(!e)return new Response(null,{status:403});const r=j.safeParse(await w(i).catch(()=>null));if(!r.success)return f(e,{ok:!1,error:{code:"invalid_input",fields:s.flattenError(r.error).fieldErrors}},400);const a=await y(r.data.password),[t]=await p.insert(n).values({email:r.data.email,firstName:r.data.firstName,lastName:r.data.lastName,passwordHash:a,role:R.affiliate,signupSource:h.self_signup,inviteAcceptedAt:null,tosAcceptedAt:new Date}).returning({id:n.id,email:n.email,firstName:n.firstName}).catch(l=>{if(T(l))return[];throw l});if(!t)return o(e,"email_already_exists",409);try{await M({userId:t.id,email:t.email,firstName:t.firstName})}catch(l){console.error("[affiliateSignup] verification email failed",{cause:l instanceof Error?l.message:String(l),userId:t.id})}return f(e,{ok:!0,next:"check_email"},201)}async function ce(i){const e=u(i);if(!e)return new Response(null,{status:403});const r=D.safeParse(await w(i).catch(()=>null));if(!r.success)return o(e,"invalid_input",400);const a=await x(r.data.token,"email_verify");if(a.kind!=="ok")return o(e,"invalid_token",400);const[t]=await p.select({id:n.id,email:n.email,firstName:n.firstName,lastName:n.lastName,status:n.status}).from(n).where(N(m(n.id,a.userId),m(n.signupSource,h.self_signup))).limit(1);if(!t||!A(t.status)||(await U(r.data.token,"email_verify")).kind!=="ok")return o(e,"invalid_token",400);const g=await O(),_=new Date;if(await p.update(n).set({inviteAcceptedAt:_,...g.requireAffiliateApproval?{}:{approvedAt:_}}).where(m(n.id,t.id)),g.requireAffiliateApproval){try{await F({affiliateEmail:t.email,affiliateFirstName:t.firstName,affiliateLastName:t.lastName})}catch(c){console.error("[affiliateVerifyEmail] pending notification failed",{cause:c instanceof Error?c.message:String(c),userId:t.id})}return f(e,{ok:!0,next:"awaiting_approval"})}try{await V({affiliateEmail:t.email,affiliateFirstName:t.firstName,affiliateLastName:t.lastName})}catch(c){console.error("[affiliateVerifyEmail] activation notification failed",{cause:c instanceof Error?c.message:String(c),userId:t.id})}return await v(t.id),await S(t.id),f(e,{ok:!0,next:"dashboard",redirectTo:d.APP_URL})}export{se as affiliateAuthConfig,oe as affiliateAuthOptions,fe as affiliateLogin,le as affiliateSignup,ce as affiliateVerifyEmail};
@@ -1 +1 @@
1
- import"server-only";import{getFormatter as p,getLocale as g,getTranslations as o}from"next-intl/server";import{AffiliateSignupVerifyEmail as d}from"../../emails/affiliate/signup-verify.js";import{issueVerificationToken as u}from"../../lib/auth/verification-tokens.js";import{formatTokenExpiry as y}from"../../lib/email/format.js";import{sendEmail as A}from"../../lib/email/send.js";import{env as n}from"../../lib/env.js";async function V(r){const a=await u({userId:r.userId,purpose:"email_verify"}),i=await g(),[m,e,s]=await Promise.all([o({locale:i,namespace:"Emails.Common"}),o({locale:i,namespace:"Emails.AffiliateSignupVerify"}),p({locale:i})]),f=new URL(`/verify-email/${a.raw}`,n.APP_URL).toString(),l=y(a.expiresAt,s),c=e("greetingNamed",{name:r.firstName}),t=n.BRAND_NAME;await A({to:r.email,subject:e("subject",{brandName:t}),react:d({verifyUrl:f,lang:i,strings:{preview:e("preview",{brandName:t}),heading:e("heading",{brandName:t}),greeting:c,body:e("body"),cta:e("cta"),expiresAtMessage:e("expiresAt",{date:l}),fallback:e("fallback"),brandName:t,footerNote:m("footerNote")}})})}export{V as sendSignupVerificationEmail};
1
+ import"server-only";import{getFormatter as p,getLocale as g,getTranslations as n}from"next-intl/server";import{AffiliateSignupVerifyEmail as d}from"../../emails/affiliate/signup-verify.js";import{issueVerificationToken as u}from"../../lib/auth/verification-tokens.js";import{formatTokenExpiry as A}from"../../lib/email/format.js";import{sendEmail as y}from"../../lib/email/send.js";import{env as a}from"../../lib/env.js";async function k(r){const o=await u({userId:r.userId,purpose:"email_verify"}),i=await g(),[m,e,s]=await Promise.all([n({locale:i,namespace:"Emails.Common"}),n({locale:i,namespace:"Emails.AffiliateSignupVerify"}),p({locale:i})]),f=new URL(`/verify-email/${o.raw}`,a.AFFILIATE_SITE_URL??a.APP_URL).toString(),l=A(o.expiresAt,s),c=e("greetingNamed",{name:r.firstName}),t=a.BRAND_NAME;await y({to:r.email,subject:e("subject",{brandName:t}),react:d({verifyUrl:f,lang:i,strings:{preview:e("preview",{brandName:t}),heading:e("heading",{brandName:t}),greeting:c,body:e("body"),cta:e("cta"),expiresAtMessage:e("expiresAt",{date:l}),fallback:e("fallback"),brandName:t,footerNote:m("footerNote")}})})}export{k as sendSignupVerificationEmail};
@@ -1 +1 @@
1
- "use server";import{db as r}from"../../db/index.js";import{USER_ROLE as p}from"../../db/schema/index.js";import{redirect as f}from"next/navigation";import{bulkApprovePendingAffiliates as m}from"../affiliates/bulk-approve.js";import{authAction as o}from"../../lib/auth/action.js";import{success as s}from"../../lib/errors/server.js";import{updatePlatformSettings as n}from"../../lib/platform-settings.js";const v=o({permission:"settings:manage",revalidate:["/settings"]},async({user:t,audit:i},a)=>(await r.transaction(async e=>{await n({requireManagerTotp:a},t.id,e),await i({action:"settings.update",target:{type:"settings",id:"requireManagerTotp"},metadata:{changed:["requireManagerTotp"]}},e)}),a&&t.role===p.manager&&!t.totpConfirmedAt&&f("/user/settings"),s("Toasts.platformSettingsUpdated"))),w=o({permission:"settings:manage",revalidate:["/settings","/affiliates"]},async({user:t,audit:i},a)=>(await r.transaction(async e=>{await n({requireAffiliateApproval:a},t.id,e),await i({action:"settings.update",target:{type:"settings",id:"requireAffiliateApproval"},metadata:{changed:["requireAffiliateApproval"]}},e)}),a||await m(t.id),s("Toasts.platformSettingsUpdated")));export{w as setRequireAffiliateApprovalAction,v as setRequireManagerTotpAction};
1
+ "use server";import{redirect as p}from"next/navigation";import{db as r}from"../../db/index.js";import{USER_ROLE as f}from"../../db/schema/index.js";import{authAction as o}from"../../lib/auth/action.js";import{success as s}from"../../lib/errors/server.js";import{updatePlatformSettings as n}from"../../lib/platform-settings.js";import{bulkApprovePendingAffiliates as m}from"../affiliates/bulk-approve.js";const v=o({permission:"settings:manage",revalidate:["/settings"]},async({user:t,audit:i},a)=>(await r.transaction(async e=>{await n({requireManagerTotp:a},t.id,e),await i({action:"settings.update",target:{type:"settings",id:"requireManagerTotp"},metadata:{changed:["requireManagerTotp"]}},e)}),a&&t.role===f.manager&&!t.totpConfirmedAt&&p("/user/settings"),s("Toasts.platformSettingsUpdated"))),w=o({permission:"settings:manage",revalidate:["/settings","/affiliates"]},async({user:t,audit:i},a)=>(await r.transaction(async e=>{await n({requireAffiliateApproval:a},t.id,e),await i({action:"settings.update",target:{type:"settings",id:"requireAffiliateApproval"},metadata:{changed:["requireAffiliateApproval"]}},e)}),a||await m(t.id),s("Toasts.platformSettingsUpdated")));export{w as setRequireAffiliateApprovalAction,v as setRequireManagerTotpAction};
package/dist/lib/env.d.ts CHANGED
@@ -8,6 +8,7 @@ declare const EnvSchema: z.ZodObject<{
8
8
  JWT_SECRET: z.ZodString;
9
9
  TOTP_ENCRYPTION_KEY: z.ZodString;
10
10
  APP_URL: z.ZodURL;
11
+ AFFILIATE_SITE_URL: z.ZodOptional<z.ZodURL>;
11
12
  BRAND_NAME: z.ZodDefault<z.ZodString>;
12
13
  TRACKER_BASE_URL: z.ZodURL;
13
14
  CLICKHOUSE_URL: z.ZodOptional<z.ZodURL>;
package/dist/lib/env.js CHANGED
@@ -1 +1 @@
1
- import{z as e}from"zod";const t=e.object({NODE_ENV:e.enum(["development","production"]).default("development"),DATABASE_URL:e.url(),JWT_SECRET:e.string().regex(/^[A-Za-z0-9_-]{64,}$/,"must be \u226564 base64url chars (use crypto.randomBytes(48).toString('base64url'))"),TOTP_ENCRYPTION_KEY:e.string().regex(/^[A-Za-z0-9_-]{43}$/,"must be 32 base64url-encoded bytes (use crypto.randomBytes(32).toString('base64url'))"),APP_URL:e.url(),BRAND_NAME:e.string().trim().min(1).default("Myfiliate"),TRACKER_BASE_URL:e.url(),CLICKHOUSE_URL:e.url().optional(),CLICKHOUSE_DATABASE:e.string().default("analytics"),CLICKHOUSE_USER:e.string().optional(),CLICKHOUSE_PASSWORD:e.string().optional()}),n=process.env.SKIP_ENV_VALIDATION==="1",s=n?t.partial():t,r=s.safeParse(process.env);if(!r.success)throw new Error(`Invalid environment variables: ${JSON.stringify(e.treeifyError(r.error))}`);const a=r.data;export{a as env};
1
+ import{z as e}from"zod";const t=e.object({NODE_ENV:e.enum(["development","production"]).default("development"),DATABASE_URL:e.url(),JWT_SECRET:e.string().regex(/^[A-Za-z0-9_-]{64,}$/,"must be \u226564 base64url chars (use crypto.randomBytes(48).toString('base64url'))"),TOTP_ENCRYPTION_KEY:e.string().regex(/^[A-Za-z0-9_-]{43}$/,"must be 32 base64url-encoded bytes (use crypto.randomBytes(32).toString('base64url'))"),APP_URL:e.url(),AFFILIATE_SITE_URL:e.url().optional(),BRAND_NAME:e.string().trim().min(1).default("Myfiliate"),TRACKER_BASE_URL:e.url(),CLICKHOUSE_URL:e.url().optional(),CLICKHOUSE_DATABASE:e.string().default("analytics"),CLICKHOUSE_USER:e.string().optional(),CLICKHOUSE_PASSWORD:e.string().optional()}),n=process.env.SKIP_ENV_VALIDATION==="1",s=n?t.partial():t,r=s.safeParse(process.env);if(!r.success)throw new Error(`Invalid environment variables: ${JSON.stringify(e.treeifyError(r.error))}`);const a=r.data;export{a as env};
@@ -725,9 +725,7 @@
725
725
  "totpCodeLabel": "Bestätigungscode",
726
726
  "totpCodeHelp": "Öffne deine Authenticator-App und gib den aktuellen Code ein.",
727
727
  "totpSubmit": "Bestätigen",
728
- "totpBack": "Zurück zur Anmeldung",
729
- "noAccount": "Neu hier?",
730
- "signupLink": "Affiliate-Konto erstellen"
728
+ "totpBack": "Zurück zur Anmeldung"
731
729
  },
732
730
  "SignUp": {
733
731
  "title": "Konto erstellen",
@@ -725,9 +725,7 @@
725
725
  "totpCodeLabel": "Verification code",
726
726
  "totpCodeHelp": "Open your authenticator app and enter the current code.",
727
727
  "totpSubmit": "Verify",
728
- "totpBack": "Back to sign-in",
729
- "noAccount": "New here?",
730
- "signupLink": "Create an affiliate account"
728
+ "totpBack": "Back to sign-in"
731
729
  },
732
730
  "SignUp": {
733
731
  "title": "Create your account",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moonhead",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
@@ -71,43 +71,9 @@
71
71
  "db:reset": "bun tools/reset-db.ts"
72
72
  },
73
73
  "peerDependencies": {
74
- "@base-ui/react": "1.4.1",
75
- "@tabler/icons-react": "3.44.0",
76
- "@tanstack/react-table": "8.21.3",
77
74
  "drizzle-orm": "1.0.0-rc.1",
78
- "next": "^16.2.4",
79
- "next-intl": "4.12.0",
80
- "nuqs": "2.8.9",
81
- "react": "^19.2.4",
82
- "react-dom": "^19.2.4",
83
75
  "zod": "^4.4.3"
84
76
  },
85
- "peerDependenciesMeta": {
86
- "@base-ui/react": {
87
- "optional": true
88
- },
89
- "@tabler/icons-react": {
90
- "optional": true
91
- },
92
- "@tanstack/react-table": {
93
- "optional": true
94
- },
95
- "next": {
96
- "optional": true
97
- },
98
- "next-intl": {
99
- "optional": true
100
- },
101
- "nuqs": {
102
- "optional": true
103
- },
104
- "react": {
105
- "optional": true
106
- },
107
- "react-dom": {
108
- "optional": true
109
- }
110
- },
111
77
  "dependencies": {
112
78
  "@dicebear/collection": "^9.4.2",
113
79
  "@dicebear/core": "^9.4.2",
@@ -1 +0,0 @@
1
- export declare function SignupForm(): import("react").JSX.Element;
@@ -1 +0,0 @@
1
- "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useTranslations as n}from"next-intl";import{useActionState as c}from"react";import{ActionForm as f}from"../../../components/ui/action-form.js";import{Button as F}from"../../../components/ui/button.js";import{Checkbox as b}from"../../../components/ui/checkbox.js";import{Field as g,FieldError as x,FieldLabel as w}from"../../../components/ui/field.js";import{FormField as a}from"../../../components/ui/form-field.js";import{Input as m}from"../../../components/ui/input.js";import{PasswordInput as s}from"../../../components/ui/password-input.js";import{TextLink as d}from"../../../components/ui/text-link.js";import{signupAction as N}from"../actions.js";function B(){const r=n("Auth.SignUp"),i=n("Common"),[p,u,l]=c(N,void 0);return t(f,{action:u,actionState:p,className:"flex flex-col gap-4",children:[e(a,{name:"email",label:r("emailLabel"),children:e(m,{name:"email",type:"email","aria-required":"true",autoComplete:"email"})}),t("div",{className:"grid gap-4 sm:grid-cols-2",children:[e(a,{name:"firstName",label:i("firstName"),children:e(m,{name:"firstName",type:"text","aria-required":"true",autoComplete:"given-name"})}),e(a,{name:"lastName",label:i("lastName"),children:e(m,{name:"lastName",type:"text","aria-required":"true",autoComplete:"family-name"})})]}),e(a,{name:"password",label:r("passwordLabel"),children:e(s,{name:"password","aria-required":"true",autoComplete:"new-password"})}),e(a,{name:"confirmPassword",label:r("confirmPasswordLabel"),children:e(s,{name:"confirmPassword","aria-required":"true",autoComplete:"new-password"})}),t(g,{name:"tosAccepted",className:"flex-row items-start gap-2",children:[e(b,{name:"tosAccepted",value:"on","aria-required":"true",className:"mt-0.5"}),t("div",{className:"flex flex-col",children:[e(w,{className:"font-normal text-sm text-muted-foreground",children:r.rich("tosAccept",{terms:o=>e(d,{href:"/legal/terms",target:"_blank",rel:"noreferrer",children:o}),privacy:o=>e(d,{href:"/legal/privacy",target:"_blank",rel:"noreferrer",children:o})})}),e(x,{})]})]}),e(F,{type:"submit",disabled:l,children:r(l?"submitting":"submit")})]})}export{B as SignupForm};