Setting the file. One moment.
Auth · Wix Headless Templates · wix/skills · Skills Docs
ContentsBack to the top of the page This file
Number 16.483
Position 483 of 828
Type TypeScript
Size 10 KB
Lines 224 members/rest/ auth.ts
TypeScript · 224 lines · 10 KB
;
12 import { WixApiError, clearTokens, loadTokens, mintTokens, setTokens, wixRequest, type Tokens } from "../../shared/rest/client.js" ;
13 import {
14 NO_SESSION_ERROR,
15 authOutcome,
16 failure,
17 loginErrorCode,
18 registerErrorCode,
19 toLoginResult,
20 type LoginResult,
21 type LoginState,
22 type RawAuth,
23 } from "../app/wix/members/auth-core.js" ;
24
25 export type { LoginResult, LoginState };
26
27 /** Same shape as the SDK transport's; on REST a SUCCESS is a session at once, so `returnTo` is the caller's to navigate. */
28 export interface LoginOptions {
29 returnTo ?: string ;
30 }
31
32 /** register() carries first/last name on this transport. */
33 export const PROFILE_ON_SIGNUP = true ;
34
35 const REDIRECT_SESSION = "/headless/v1/redirect-session" ;
36 /** The stateToken of a login/registration that stopped at email verification — verifyMemberEmail continues it. */
37 let pendingStateToken : string | null = null ;
38
39 /** True when the persisted token set is a member's (no network) — the store skips the profile read when false. */
40 export function loggedInHint () : boolean {
41 return loadTokens ()?.role === "member" ;
42 }
43
44 // A SUCCESS carries the one-shot session token: exchange it for member tokens and adopt them. Other
45 // states pass through; a verification state keeps its stateToken for verifyMemberEmail.
46 async function finish ( raw : RawAuth ) : Promise < LoginResult > {
47 const outcome = authOutcome (raw);
48 if (outcome.state === "SUCCESS" ) {
49 pendingStateToken = null ;
50 if ( ! outcome.sessionToken) return failure ( NO_SESSION_ERROR );
51 setTokens ( await memberTokensForSession (outcome.sessionToken));
52 return { state: "SUCCESS" };
53 }
54 if (outcome.stateToken) pendingStateToken = outcome.stateToken;
55 return toLoginResult (outcome);
56 }
57
58 const message = ( e : unknown ) : string => (e instanceof Error ? e.message : String (e));
59
60 /**
61 * Sign in with email + password. A wrong password or unknown email is a FAILURE result (errorCode
62 * invalidPassword / invalidEmail, Wix's message in `error`), never a throw.
63 * POST /_api/iam/authentication/v2/login { loginId: { email }, password } → { state, sessionToken?, stateToken? }
64 */
65 export async function loginMember ( email : string , password : string , _options : LoginOptions = {}) : Promise < LoginResult > {
66 let raw : RawAuth ;
67 try {
68 raw = await wixRequest < RawAuth >( "/_api/iam/authentication/v2/login" , { body: { loginId: { email }, password } });
69 } catch (e) {
70 return failure ( message (e), e instanceof WixApiError ? loginErrorCode (e.code) : undefined );
71 }
72 return finish (raw);
73 }
74
75 /**
76 * Sign up with email + password (+ first/last name). An email already registered is a FAILURE with
77 * errorCode emailAlreadyExists. Whether the result is SUCCESS, EMAIL_VERIFICATION_REQUIRED, or
78 * OWNER_APPROVAL_REQUIRED is the site's signup policy (dashboard), not the caller's.
79 * POST /_api/iam/authentication/v2/register { loginId: { email }, password, profile?: { firstName, lastName } }
80 */
81 export async function registerMember (
82 email : string ,
83 password : string ,
84 profile ?: { firstName ?: string ; lastName ?: string },
85 _options : LoginOptions = {},
86 ) : Promise < LoginResult > {
87 let raw : RawAuth ;
88 try {
89 raw = await wixRequest < RawAuth >( "/_api/iam/authentication/v2/register" , {
90 body: { loginId: { email }, password, ... (profile ? { profile } : {}) },
91 });
92 } catch (e) {
93 return failure ( message (e), e instanceof WixApiError ? registerErrorCode (e.code, e.details) : undefined );
94 }
95 return finish (raw);
96 }
97
98 /**
99 * Continue an EMAIL_VERIFICATION_REQUIRED flow with the code from the email; the state token of the
100 * last login/registration is kept here. A FAILURE when nothing is waiting or the code is wrong.
101 * POST /_api/iam/verification/v1/auth/verify { code, stateToken } → { state, sessionToken?, stateToken? }
102 */
103 export async function verifyMemberEmail ( verificationCode : string ) : Promise < LoginResult > {
104 if ( ! pendingStateToken) return failure ( "No sign-in is waiting for a verification code." );
105 let raw : RawAuth ;
106 try {
107 raw = await wixRequest < RawAuth >( "/_api/iam/verification/v1/auth/verify" , { body: { code: verificationCode, stateToken: pendingStateToken } });
108 } catch (e) {
109 return failure ( message (e));
110 }
111 return finish (raw);
112 }
113
114 /**
115 * The Wix logout URL for the current member — navigating the document there ends the Wix session
116 * and lands on `returnTo` (an absolute URL). Called with the MEMBER token.
117 * POST /headless/v1/redirect-session { logout: { clientId }, callbacks: { postFlowUrl } } → { redirectSession: { fullUrl } }
118 */
119 export async function logoutUrl ( returnTo : string ) : Promise < string > {
120 const res = await wixRequest < RawAuth >( REDIRECT_SESSION , { body: { logout: { clientId: WIX_CLIENT_ID }, callbacks: { postFlowUrl: returnTo } } });
121 const url = res?.redirectSession?.fullUrl;
122 if ( ! url) throw new Error ( "Logout couldn't start: no redirect URL returned." );
123 return url;
124 }
125
126 /** Log out: drop the member tokens (the next call is a fresh visitor) and navigate to Wix's logout, which returns to `returnTo`. */
127 export async function logoutMember ( returnTo = "/" ) : Promise < void > {
128 const target = typeof window !== "undefined" ? new URL (returnTo, window.location.origin).href : returnTo;
129 const url = await logoutUrl (target);
130 clearTokens ();
131 if ( typeof window !== "undefined" ) window.location. assign (url);
132 }
133
134 /**
135 * sessionToken → member tokens. Three calls, as the docs order them:
136 * 1. POST /headless/v1/redirect-session
137 * { auth: { authRequest: { clientId, codeChallenge, codeChallengeMethod: "S256", responseMode: "web_message",
138 * responseType: "code", scope: "offline_access", state, sessionToken }, prompt: "none" } }
139 * → { redirectSession: { fullUrl } } (with the visitor token)
140 * 2. fullUrl is a page whose script posts { code, state, error } to its parent — a hidden iframe in a
141 * browser (its origin must be one of the OAuth app's allowed domains, or the message is dropped
142 * and this times out); without a DOM (a server-side port, a Node check) the same page is fetched
143 * and read.
144 * 3. POST /oauth2/token { clientId, grantType: "authorization_code", code, codeVerifier }
145 * → member access + refresh tokens (role "member"); the code is single-use.
146 */
147 export async function memberTokensForSession ( sessionToken : string ) : Promise < Tokens > {
148 const { verifier , challenge } = await pkce ();
149 const state = randomString ();
150 const res = await wixRequest < RawAuth >( REDIRECT_SESSION , {
151 body: {
152 auth: {
153 authRequest: {
154 clientId: WIX_CLIENT_ID ,
155 codeChallenge: challenge,
156 codeChallengeMethod: "S256" ,
157 responseMode: "web_message" ,
158 responseType: "code" ,
159 scope: "offline_access" ,
160 state,
161 sessionToken,
162 },
163 prompt: "none" ,
164 },
165 },
166 });
167 const url : string | undefined = res?.redirectSession?.fullUrl;
168 if ( ! url) throw new Error ( "Login couldn't finish: no authorization URL returned." );
169 const code = await authorizationCode (url, state);
170 return mintTokens ({ grantType: "authorization_code" , code, codeVerifier: verifier }, "member" );
171 }
172
173 // The authorize page posts { code, state, error }. Browser: a hidden iframe and a message listener
174 // keyed on our state (the SDK's own mechanism). No DOM: fetch the page and read the same three values.
175 async function authorizationCode ( url : string , state : string ) : Promise < string > {
176 if ( typeof document === "undefined" ) {
177 const html = await ( await fetch (url)). text ();
178 const field = ( name : string ) => html. match ( new RegExp ( `'${ name }': \\ s*'([^']*)'` ))?.[ 1 ];
179 const error = field ( "error" );
180 if (error) throw new Error ( `Login was refused: ${ error }` );
181 const code = field ( "code" );
182 if ( ! code || field ( "state" ) !== state) throw new Error ( "Login couldn't finish: no authorization code." );
183 return code;
184 }
185 return new Promise < string >(( resolve , reject ) => {
186 const frame = document. createElement ( "iframe" );
187 frame.style.display = "none" ;
188 const done = () => {
189 window. removeEventListener ( "message" , onMessage);
190 clearTimeout (timer);
191 frame. remove ();
192 };
193 const onMessage = ( e : MessageEvent ) => {
194 const data = e.data as { code ?: string ; state ?: string ; error ?: string } | null ;
195 if ( ! data || data.state !== state) return ; // a message not meant for us
196 done ();
197 if (data.error) reject ( new Error ( `Login was refused: ${ data . error }` ));
198 else if (data.code) resolve (data.code);
199 else reject ( new Error ( "Login couldn't finish: no authorization code." ));
200 };
201 const timer = setTimeout (() => {
202 done ();
203 reject ( new Error ( "Login timed out. Is this page's origin on the OAuth app's allowed domains?" ));
204 }, 120_000 );
205 window. addEventListener ( "message" , onMessage);
206 frame.src = url;
207 document.body. appendChild (frame);
208 });
209 }
210
211 // PKCE: a 43-char base64url verifier, its SHA-256 as the S256 challenge. Web Crypto (browser + Node).
212 function base64url ( bytes : Uint8Array ) : string {
213 let s = "" ;
214 for ( const b of bytes) s += String. fromCharCode (b);
215 return btoa (s). replace ( / \+ / g , "-" ). replace ( / \/ / g , "_" ). replace ( /= +$ / , "" );
216 }
217 function randomString () : string {
218 return base64url (crypto. getRandomValues ( new Uint8Array ( 32 )));
219 }
220 async function pkce () : Promise <{ verifier : string ; challenge : string }> {
221 const verifier = randomString ();
222 const digest = await crypto.subtle. digest ( "SHA-256" , new TextEncoder (). encode (verifier));
223 return { verifier, challenge: base64url ( new Uint8Array (digest)) };
224 }