moonhead 1.0.3 → 1.0.5

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};
@@ -0,0 +1,3 @@
1
+ export declare function CampaignDetailTabs({ hidePerformance, }: {
2
+ hidePerformance?: boolean;
3
+ }): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as D}from"react/jsx-runtime";import{useTranslations as T}from"next-intl";import{useQueryStates as l}from"nuqs";import{useTransition as m}from"react";import{Tabs as f,TabsIndicator as p,TabsList as u,TabsTab as c}from"../../../components/ui/tabs.js";import{CAMPAIGN_DETAIL_TABS as g,campaignDetailTabParser as d}from"../list-params.js";function I({hidePerformance:t=!1}){const s=T("Campaigns.DetailTabs"),[i,n]=m(),[{tab:r},o]=l({tab:d},{shallow:!1,clearOnDefault:!0,startTransition:n}),b=g.filter(a=>!(t&&a==="performance"));return e(f,{value:r,onValueChange:a=>o({tab:a}),"data-pending":i?"":void 0,children:D(u,{children:[b.map(a=>e(c,{value:a,children:s(a)},a)),e(p,{})]})})}export{I as CampaignDetailTabs};
@@ -1 +1 @@
1
- "use client";import{Fragment as L,jsx as t,jsxs as l}from"react/jsx-runtime";import{IconSpeakerphone as f}from"@tabler/icons-react";import{useFormatter as x,useTranslations as D}from"next-intl";import{useCallback as S,useMemo as y,useRef as N}from"react";import{DataTable as k}from"../../../components/ui/data-table.js";import{Identity as T}from"../../../components/ui/identity.js";import{SortButton as h}from"../../../components/ui/sort-button.js";import{TablePagination as b}from"../../../components/ui/table-pagination.js";import{TextButton as A}from"../../../components/ui/text-link.js";import{CampaignRowActions as v}from"./campaign-row-actions.js";import{CampaignStatusBadge as I}from"./campaign-status-badge.js";import{TrackingLinkCell as z}from"./tracking-link-cell.js";import{getCommissionForDealView as B}from"../../commissions/actions.js";import{CommissionDetailDialog as P}from"../../commissions/components/commission-detail-dialog.js";import{useDeferredDialog as R}from"../../../lib/hooks/use-deferred-dialog.js";import{PAGE_SIZE_OPTIONS as E}from"../../../lib/list-params.js";function X({result:n,canManage:o,trackerBase:s}){const a=D("Campaigns"),m=x(),r=N(null),c=R(()=>r.current?B(r.current):Promise.resolve(null)),u=c.trigger,d=S(i=>{r.current=i,u()},[u]),g=o,C=y(()=>{const i=e=>m.dateTime(e,{day:"numeric",month:"short",year:"numeric"}),p=[{id:"name",header:()=>t(h,{column:"name",children:a("columnName")}),cell:({row:e})=>t(T,{visual:t("div",{className:"flex size-9 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground",children:t(f,{size:18,"aria-hidden":!0})}),primary:e.original.name,secondary:e.original.description?t("span",{className:"line-clamp-1 text-xs text-muted-foreground",children:e.original.description}):null})},{id:"status",header:()=>a("columnStatus"),cell:({row:e})=>t(I,{status:e.original.status,dealStatus:e.original.dealStatus,commissionStatus:e.original.commissionStatus})},{id:"casino",header:()=>a("columnCasino"),cell:({row:e})=>t("span",{className:"text-sm",children:e.original.casinoName})},{id:"commission",header:()=>a("columnCommission"),cell:({row:e})=>t(A,{onClick:()=>d(e.original.commissionId),children:e.original.commissionTitle??a("untitledCommission")})},{id:"deal",header:()=>a("columnDeal"),cell:({row:e})=>e.original.dealStartsAt?l("span",{className:"text-sm text-muted-foreground tabular-nums",children:[i(e.original.dealStartsAt)," \u2013"," ",e.original.dealEndsAt?i(e.original.dealEndsAt):a("dealOpenEnded")]}):t("span",{className:"text-sm text-muted-foreground",children:"\u2014"})},{id:"createdAt",header:()=>t(h,{column:"createdAt",children:a("columnCreated")}),cell:({row:e})=>t("span",{className:"text-sm text-muted-foreground tabular-nums",children:i(e.original.createdAt)})},{id:"trackingLink",header:()=>a("columnTrackingLink"),cell:({row:e})=>t(z,{slug:e.original.slug,trackerBase:s}),meta:{className:"text-right"}}];return g?[...p,{id:"actions",header:()=>t("span",{className:"sr-only",children:a("columnActions")}),cell:({row:e})=>t(v,{campaignId:e.original.id,slug:e.original.slug,status:e.original.status,canManage:o,trackerBase:s}),meta:{pin:"right",className:"text-right"}}]:p},[a,m,o,g,s,d]);return l(L,{children:[t(k,{data:n.rows,columns:C,getRowId:i=>i.id,empty:l("div",{className:"flex flex-col items-center gap-3 py-4",children:[t("div",{className:"flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground",children:t(f,{size:18,"aria-hidden":!0})}),t("span",{children:a("emptyState")})]}),pagination:t(b,{page:n.page,pageSize:n.pageSize,totalPages:n.totalPages,pageSizeOptions:E})}),t(P,{controller:c})]})}export{X as CampaignsTable};
1
+ "use client";import{Fragment as O,jsx as t,jsxs as l}from"react/jsx-runtime";import{IconSpeakerphone as f}from"@tabler/icons-react";import{useFormatter as C,useTranslations as D}from"next-intl";import{useCallback as S,useMemo as k,useRef as y}from"react";import{DataTable as N}from"../../../components/ui/data-table.js";import{Identity as T}from"../../../components/ui/identity.js";import{SortButton as h}from"../../../components/ui/sort-button.js";import{TablePagination as b}from"../../../components/ui/table-pagination.js";import{TextButton as A,TextLink as v}from"../../../components/ui/text-link.js";import{CampaignRowActions as I}from"./campaign-row-actions.js";import{CampaignStatusBadge as z}from"./campaign-status-badge.js";import{TrackingLinkCell as B}from"./tracking-link-cell.js";import{getCommissionForDealView as L}from"../../commissions/actions.js";import{CommissionDetailDialog as P}from"../../commissions/components/commission-detail-dialog.js";import{useDeferredDialog as R}from"../../../lib/hooks/use-deferred-dialog.js";import{PAGE_SIZE_OPTIONS as E}from"../../../lib/list-params.js";function X({result:n,canManage:o,trackerBase:s}){const a=D("Campaigns"),m=C(),r=y(null),c=R(()=>r.current?L(r.current):Promise.resolve(null)),u=c.trigger,d=S(i=>{r.current=i,u()},[u]),g=o,x=k(()=>{const i=e=>m.dateTime(e,{day:"numeric",month:"short",year:"numeric"}),p=[{id:"name",header:()=>t(h,{column:"name",children:a("columnName")}),cell:({row:e})=>t(T,{visual:t("div",{className:"flex size-9 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground",children:t(f,{size:18,"aria-hidden":!0})}),primary:t(v,{href:`/campaigns/${e.original.id}`,tone:"plain",children:e.original.name}),secondary:e.original.description?t("span",{className:"line-clamp-1 text-xs text-muted-foreground",children:e.original.description}):null})},{id:"status",header:()=>a("columnStatus"),cell:({row:e})=>t(z,{status:e.original.status,dealStatus:e.original.dealStatus,commissionStatus:e.original.commissionStatus})},{id:"casino",header:()=>a("columnCasino"),cell:({row:e})=>t("span",{className:"text-sm",children:e.original.casinoName})},{id:"commission",header:()=>a("columnCommission"),cell:({row:e})=>t(A,{onClick:()=>d(e.original.commissionId),children:e.original.commissionTitle??a("untitledCommission")})},{id:"deal",header:()=>a("columnDeal"),cell:({row:e})=>e.original.dealStartsAt?l("span",{className:"text-sm text-muted-foreground tabular-nums",children:[i(e.original.dealStartsAt)," \u2013"," ",e.original.dealEndsAt?i(e.original.dealEndsAt):a("dealOpenEnded")]}):t("span",{className:"text-sm text-muted-foreground",children:"\u2014"})},{id:"createdAt",header:()=>t(h,{column:"createdAt",children:a("columnCreated")}),cell:({row:e})=>t("span",{className:"text-sm text-muted-foreground tabular-nums",children:i(e.original.createdAt)})},{id:"trackingLink",header:()=>a("columnTrackingLink"),cell:({row:e})=>t(B,{slug:e.original.slug,trackerBase:s}),meta:{className:"text-right"}}];return g?[...p,{id:"actions",header:()=>t("span",{className:"sr-only",children:a("columnActions")}),cell:({row:e})=>t(I,{campaignId:e.original.id,slug:e.original.slug,status:e.original.status,canManage:o,trackerBase:s}),meta:{pin:"right",className:"text-right"}}]:p},[a,m,o,g,s,d]);return l(O,{children:[t(N,{data:n.rows,columns:x,getRowId:i=>i.id,empty:l("div",{className:"flex flex-col items-center gap-3 py-4",children:[t("div",{className:"flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground",children:t(f,{size:18,"aria-hidden":!0})}),t("span",{children:a("emptyState")})]}),pagination:t(b,{page:n.page,pageSize:n.pageSize,totalPages:n.totalPages,pageSizeOptions:E})}),t(P,{controller:c})]})}export{X as CampaignsTable};
@@ -12,3 +12,15 @@ export declare const loadCampaignStatus: import("nuqs/server").LoaderFunction<{
12
12
  parseServerSide(value: string | string[] | undefined): NonNullable<"active" | "deactivated" | "all">;
13
13
  };
14
14
  }>;
15
+ export declare const CAMPAIGN_DETAIL_TABS: readonly ["overview", "performance"];
16
+ export type CampaignDetailTab = (typeof CAMPAIGN_DETAIL_TABS)[number];
17
+ export declare const campaignDetailTabParser: Omit<import("nuqs/server").SingleParserBuilder<"performance" | "overview">, "parseServerSide"> & {
18
+ readonly defaultValue: NonNullable<"performance" | "overview">;
19
+ parseServerSide(value: string | string[] | undefined): NonNullable<"performance" | "overview">;
20
+ };
21
+ export declare const loadCampaignDetailTab: import("nuqs/server").LoaderFunction<{
22
+ tab: Omit<import("nuqs/server").SingleParserBuilder<"performance" | "overview">, "parseServerSide"> & {
23
+ readonly defaultValue: NonNullable<"performance" | "overview">;
24
+ parseServerSide(value: string | string[] | undefined): NonNullable<"performance" | "overview">;
25
+ };
26
+ }>;
@@ -1 +1 @@
1
- import{createLoader as t,parseAsStringLiteral as a}from"nuqs/server";const p=["createdAt","name"],e=["all","active","deactivated"],r=a(e).withDefault("active"),s=t({status:r});export{p as CAMPAIGN_SORT_KEYS,e as CAMPAIGN_STATUSES,r as campaignStatusParser,s as loadCampaignStatus};
1
+ import{createLoader as t,parseAsStringLiteral as a}from"nuqs/server";const s=["createdAt","name"],e=["all","active","deactivated"],o=a(e).withDefault("active"),i=t({status:o}),r=["overview","performance"],p=a(r).withDefault("overview"),A=t({tab:p});export{r as CAMPAIGN_DETAIL_TABS,s as CAMPAIGN_SORT_KEYS,e as CAMPAIGN_STATUSES,p as campaignDetailTabParser,o as campaignStatusParser,A as loadCampaignDetailTab,i as loadCampaignStatus};
@@ -0,0 +1,8 @@
1
+ import "server-only";
2
+ import { type ConversionStats, type Delta, type MoneyByCurrency } from "./queries.js";
3
+ import { type MoneySeries, type PerformanceSeries } from "./series.js";
4
+ export declare const getCampaignConversionStats: (campaignId: string, startDate: string, endDate: string) => Promise<ConversionStats>;
5
+ export declare const getCampaignEarnings: (campaignId: string, startDate: string, endDate: string) => Promise<MoneyByCurrency[]>;
6
+ export declare const getCampaignClickStats: (campaignId: string, startDate: string, endDate: string) => Promise<Delta | null>;
7
+ export declare const getCampaignPerformanceSeries: (campaignId: string, startDate: string, endDate: string) => Promise<PerformanceSeries>;
8
+ export declare const getCampaignEarningsSeries: (campaignId: string, startDate: string, endDate: string) => Promise<MoneySeries>;
@@ -0,0 +1 @@
1
+ import"server-only";import{and as d,eq as m,gte as p,inArray as O,lt as v,ne as U,sum as B}from"drizzle-orm";import{unstable_cache as l}from"next/cache";import{db as f}from"../../db/index.js";import{CONVERSION_EVENT as A,conversions as e,EARNING_STATUS as R,earningsLedger as r}from"../../db/schema/index.js";import{clickHouseRows as L,clickHouseScalar as w}from"./clickhouse.js";import{countRows as g,windowBounds as y}from"./queries.js";import{assembleMoney as T,assemblePerformance as x,eachDay as I}from"./series.js";const S=60,W=l(async(s,n,t)=>{const{start:o,end:a,prevStart:c}=y(n,t),i=m(e.campaignId,s),u=d(i,p(e.receivedAt,o),v(e.receivedAt,a)),C=d(i,p(e.receivedAt,c),v(e.receivedAt,o)),E=m(e.event,A.ftd),P=m(e.event,A.registration),[N,M,b,h,k,D]=await Promise.all([g(e,u),g(e,C),g(e,d(E,u)),g(e,d(E,C)),g(e,d(P,u)),g(e,d(P,C))]);return{conversions:{current:N,previous:M},ftd:{current:b,previous:h},registrations:{current:k,previous:D}}},["dashboard:campaign-conversion-stats"],{revalidate:S}),j=l(async(s,n,t)=>{const{start:o,end:a}=y(n,t);return(await f.select({currency:r.currency,amount:B(r.amount)}).from(r).innerJoin(e,m(e.id,r.conversionId)).where(d(m(e.campaignId,s),p(r.earnedAt,o),v(r.earnedAt,a),m(r.status,R.confirmed))).groupBy(r.currency)).map(i=>({currency:i.currency,amount:i.amount??"0"}))},["dashboard:campaign-earnings"],{revalidate:S}),_="SELECT uniqMerge(click_count) FROM clicks_daily WHERE date >= {s:Date} AND date < {e:Date} AND campaign_id = {c:UUID}",z=l(async(s,n,t)=>{const{prevStart:o}=y(n,t),a=o.toISOString().slice(0,10),[c,i]=await Promise.all([w(_,{s:n,e:t,c:s}),w(_,{s:a,e:n,c:s})]);return c===null?null:{current:c,previous:i??0}},["dashboard:campaign-click-stats"],{revalidate:S}),G="SELECT toString(date), toString(uniqMerge(click_count)) FROM clicks_daily WHERE date >= {s:Date} AND date < {e:Date} AND campaign_id = {c:UUID} GROUP BY date ORDER BY date",X=l(async(s,n,t)=>{const{start:o,end:a}=y(n,t),c=I(n,t),[i,u]=await Promise.all([L(G,{s:n,e:t,c:s}),f.select({receivedAt:e.receivedAt,event:e.event}).from(e).where(d(m(e.campaignId,s),p(e.receivedAt,o),v(e.receivedAt,a),O(e.event,[A.registration,A.ftd])))]);return x(c,i,u)},["dashboard:campaign-performance-series"],{revalidate:S}),Z=l(async(s,n,t)=>{const{start:o,end:a}=y(n,t),c=I(n,t),i=await f.select({earnedAt:r.earnedAt,amount:r.amount,currency:r.currency}).from(r).innerJoin(e,m(e.id,r.conversionId)).where(d(m(e.campaignId,s),p(r.earnedAt,o),v(r.earnedAt,a),U(r.status,R.reversed)));return T(c,i)},["dashboard:campaign-earnings-series"],{revalidate:S});export{z as getCampaignClickStats,W as getCampaignConversionStats,j as getCampaignEarnings,Z as getCampaignEarningsSeries,X as getCampaignPerformanceSeries};
@@ -0,0 +1,5 @@
1
+ export declare function CampaignCharts({ campaignId, startDate, endDate, }: {
2
+ campaignId: string;
3
+ startDate: string;
4
+ endDate: string;
5
+ }): Promise<import("react").JSX.Element>;
@@ -0,0 +1 @@
1
+ import{jsx as o}from"react/jsx-runtime";import{getCampaignEarningsSeries as t,getCampaignPerformanceSeries as s}from"../campaign-queries.js";import{ChartsGrid as g}from"./charts-grid.js";async function f({campaignId:r,startDate:n,endDate:i}){const[e,a]=await Promise.all([s(r,n,i),t(r,n,i)]);return o(g,{perf:e,money:a,moneyTitleKey:"chartEarningsTitle"})}export{f as CampaignCharts};
@@ -0,0 +1,5 @@
1
+ export declare function CampaignStats({ campaignId, startDate, endDate, }: {
2
+ campaignId: string;
3
+ startDate: string;
4
+ endDate: string;
5
+ }): Promise<import("react").JSX.Element>;
@@ -0,0 +1 @@
1
+ import{jsx as r,jsxs as v}from"react/jsx-runtime";import{getTranslations as g}from"next-intl/server";import{getCampaignClickStats as c,getCampaignConversionStats as p,getCampaignEarnings as d}from"../campaign-queries.js";import{MoneyStatCard as m}from"./money-stat-card.js";import{StatCard as t}from"./stat-card.js";async function f({campaignId:n,startDate:s,endDate:e}){const[a,i,l,o]=await Promise.all([g("Dashboard"),p(n,s,e),d(n,s,e),c(n,s,e)]);return v("div",{className:"grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5",children:[o&&r(t,{label:a("kpiClicks"),value:o.current,delta:o}),r(t,{label:a("kpiConversions"),value:i.conversions.current,delta:i.conversions}),r(t,{label:a("kpiFtd"),value:i.ftd.current,delta:i.ftd}),r(t,{label:a("kpiRegistrations"),value:i.registrations.current,delta:i.registrations}),r(m,{label:a("kpiEarnings"),entries:l,emptyLabel:a("noData")})]})}export{f as CampaignStats};
@@ -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};
@@ -301,6 +301,10 @@
301
301
  "editDialogDesc": "Ändere den Namen des Casinos."
302
302
  },
303
303
  "Campaigns": {
304
+ "DetailTabs": {
305
+ "overview": "Übersicht",
306
+ "performance": "Performance"
307
+ },
304
308
  "Detail": {
305
309
  "backToList": "Zurück zu Kampagnen",
306
310
  "trackingLinkTitle": "Tracking-Link",
@@ -725,9 +729,7 @@
725
729
  "totpCodeLabel": "Bestätigungscode",
726
730
  "totpCodeHelp": "Öffne deine Authenticator-App und gib den aktuellen Code ein.",
727
731
  "totpSubmit": "Bestätigen",
728
- "totpBack": "Zurück zur Anmeldung",
729
- "noAccount": "Neu hier?",
730
- "signupLink": "Affiliate-Konto erstellen"
732
+ "totpBack": "Zurück zur Anmeldung"
731
733
  },
732
734
  "SignUp": {
733
735
  "title": "Konto erstellen",
@@ -301,6 +301,10 @@
301
301
  "editDialogDesc": "Update the casino's name."
302
302
  },
303
303
  "Campaigns": {
304
+ "DetailTabs": {
305
+ "overview": "Overview",
306
+ "performance": "Performance"
307
+ },
304
308
  "Detail": {
305
309
  "backToList": "Back to campaigns",
306
310
  "trackingLinkTitle": "Tracking link",
@@ -725,9 +729,7 @@
725
729
  "totpCodeLabel": "Verification code",
726
730
  "totpCodeHelp": "Open your authenticator app and enter the current code.",
727
731
  "totpSubmit": "Verify",
728
- "totpBack": "Back to sign-in",
729
- "noAccount": "New here?",
730
- "signupLink": "Create an affiliate account"
732
+ "totpBack": "Back to sign-in"
731
733
  },
732
734
  "SignUp": {
733
735
  "title": "Create your account",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moonhead",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
@@ -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};