UNPKG

@replyke/core

Version:

Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.

285 lines 11.9 kB
import { createAsyncThunk } from "@reduxjs/toolkit"; import axios from "../../config/axios"; import { handleError } from "../../utils/handleError"; import { setTokens, setUser, setAuthenticating, setInitialized, resetAuth, } from "./authSlice"; import { setUser as setUserInUserSlice, clearUser as clearUserInUserSlice, } from "./userSlice"; import { removeAccount, clearAllAccounts } from "./accountsSlice"; import { baseApi } from "../api/baseApi"; // Auth service functions - calling existing API patterns directly const authService = { async signUpWithEmailAndPassword(projectId, data) { // Check if we need to use FormData (when files are present) if (data.avatarFile || data.bannerFile) { const formData = new FormData(); // Append regular fields formData.append("email", data.email); formData.append("password", data.password); if (data.name?.trim()) formData.append("name", data.name.trim()); if (data.username?.trim()) formData.append("username", data.username.trim()); if (data.bio?.trim()) formData.append("bio", data.bio.trim()); if (data.location) formData.append("location", JSON.stringify(data.location)); if (data.birthdate) formData.append("birthdate", data.birthdate.toISOString()); if (data.metadata) formData.append("metadata", JSON.stringify(data.metadata)); if (data.secureMetadata) formData.append("secureMetadata", JSON.stringify(data.secureMetadata)); // Append avatar file and options if (data.avatarFile) { formData.append("avatarFile", data.avatarFile); if (data.avatarOptions) { formData.append("avatarFile.options", JSON.stringify(data.avatarOptions)); } } // Append banner file and options if (data.bannerFile) { formData.append("bannerFile", data.bannerFile); if (data.bannerOptions) { formData.append("bannerFile.options", JSON.stringify(data.bannerOptions)); } } const response = await axios.post(`/${projectId}/auth/sign-up`, formData, { headers: { "Content-Type": "multipart/form-data" }, }); return response.data; } // Fallback to regular JSON request (backward compatibility) const response = await axios.post(`/${projectId}/auth/sign-up`, { email: data.email, password: data.password, name: data.name?.trim(), username: data.username?.trim(), avatar: data.avatar, bio: data.bio?.trim(), location: data.location, birthdate: data.birthdate, metadata: data.metadata, secureMetadata: data.secureMetadata, }); return response.data; }, async signInWithEmailAndPassword(projectId, data) { const response = await axios.post(`/${projectId}/auth/sign-in`, data); return response.data; }, async signOut(projectId, refreshToken) { const payload = refreshToken ? { refreshToken } : {}; await axios.post(`/${projectId}/auth/sign-out`, payload); }, async requestNewAccessToken(projectId, refreshToken) { const payload = refreshToken ? { refreshToken } : {}; const response = await axios.post(`/${projectId}/auth/request-new-access-token`, payload); return response.data; }, async verifyExternalUser(projectId, userJwt) { const response = await axios.post(`/${projectId}/auth/verify-external-user`, { userJwt }); return response.data; }, async changePassword(projectId, data) { await axios.post(`/${projectId}/auth/change-password`, data); }, }; // Async Thunks export const signUpWithEmailAndPasswordThunk = createAsyncThunk("auth/signUpWithEmailAndPassword", async (data, { dispatch, rejectWithValue }) => { try { dispatch(setAuthenticating(true)); const result = await authService.signUpWithEmailAndPassword(data.projectId, data); // Update auth state dispatch(setTokens({ accessToken: result.accessToken, refreshToken: result.refreshToken, })); dispatch(setUser(result.user)); dispatch(setUserInUserSlice(result.user)); // Sync user to user slice return result; } catch (error) { handleError(error, "Failed to register user with email and password:"); return rejectWithValue(error instanceof Error ? error.message : "Unknown error"); } finally { dispatch(setAuthenticating(false)); } }); export const signInWithEmailAndPasswordThunk = createAsyncThunk("auth/signInWithEmailAndPassword", async (data, { dispatch, rejectWithValue }) => { try { dispatch(setAuthenticating(true)); const result = await authService.signInWithEmailAndPassword(data.projectId, data); // Update auth state dispatch(setTokens({ accessToken: result.accessToken, refreshToken: result.refreshToken, })); dispatch(setUser(result.user)); dispatch(setUserInUserSlice(result.user)); // Sync user to user slice return result; } catch (error) { handleError(error, "Failed to log user in:"); return rejectWithValue(error instanceof Error ? error.message : "Unknown error"); } finally { dispatch(setAuthenticating(false)); } }); export const signOutThunk = createAsyncThunk("auth/signOut", async (data, { dispatch, getState, rejectWithValue }) => { const state = getState(); const refreshToken = state.replyke.auth.refreshToken; const activeAccountId = state.replyke.accounts.activeAccountId; const accounts = state.replyke.accounts.accounts; if (!refreshToken) { throw new Error("No refresh token"); } try { dispatch(setAuthenticating(true)); await authService.signOut(data.projectId, refreshToken); // Remove current account from the multi-account map if (activeAccountId) { dispatch(removeAccount(activeAccountId)); } // Check for remaining accounts const remainingIds = Object.keys(accounts).filter((id) => id !== activeAccountId); if (remainingIds.length > 0) { // Switch to the first remaining account const nextId = remainingIds[0]; const nextAccount = accounts[nextId]; dispatch(resetAuth()); dispatch(clearUserInUserSlice()); dispatch(baseApi.util.resetApiState()); dispatch(setTokens({ accessToken: null, refreshToken: nextAccount.refreshToken, })); dispatch(setInitialized(false)); await dispatch(requestNewAccessTokenThunk({ projectId: data.projectId })); dispatch(setInitialized(true)); } else { // No remaining accounts — standard sign-out dispatch(resetAuth()); dispatch(clearUserInUserSlice()); dispatch(baseApi.util.resetApiState()); } return; } catch (error) { handleError(error, "Failed to log user out:"); return rejectWithValue(error instanceof Error ? error.message : "Unknown error"); } finally { dispatch(setAuthenticating(false)); } }); export const requestNewAccessTokenThunk = createAsyncThunk("auth/requestNewAccessToken", async (data, { dispatch, getState, rejectWithValue }) => { const state = getState(); const refreshToken = state.replyke.auth.refreshToken; if (!refreshToken) { return; } try { const result = await authService.requestNewAccessToken(data.projectId, refreshToken); // Update auth state (store rotated refresh token from server) dispatch(setTokens({ accessToken: result.accessToken, refreshToken: result.refreshToken, })); dispatch(setUser(result.user)); dispatch(setUserInUserSlice(result.user)); // Sync user to user slice return result.accessToken; } catch (error) { handleError(error, "Request new access token error:"); return rejectWithValue(error instanceof Error ? error.message : "Unknown error"); } }); export const verifyExternalUserThunk = createAsyncThunk("auth/verifyExternalUser", async (data, { dispatch, rejectWithValue }) => { try { const result = await authService.verifyExternalUser(data.projectId, data.userJwt); // Update auth state dispatch(setTokens({ accessToken: result.accessToken, refreshToken: result.refreshToken, })); dispatch(setUser(result.user)); dispatch(setUserInUserSlice(result.user)); // Sync user to user slice return result; } catch (error) { handleError(error, "Verify external user error:"); return rejectWithValue(error instanceof Error ? error.message : "Unknown error"); } }); export const changePasswordThunk = createAsyncThunk("auth/changePassword", async (data, { dispatch, getState, rejectWithValue }) => { const state = getState(); if (!state.replyke.auth.user) { throw new Error("No user is authenticated"); } try { dispatch(setAuthenticating(true)); await authService.changePassword(data.projectId, data); return; } catch (error) { handleError(error, "Failed to change password:"); return rejectWithValue(error instanceof Error ? error.message : "Unknown error"); } finally { dispatch(setAuthenticating(false)); } }); export const signOutAllThunk = createAsyncThunk("auth/signOutAll", async (data, { dispatch, getState, rejectWithValue }) => { const state = getState(); const accounts = state.replyke.accounts.accounts; try { dispatch(setAuthenticating(true)); // Sign out from each account on the server (best-effort) const signOutPromises = Object.values(accounts).map(async (account) => { try { await authService.signOut(data.projectId, account.refreshToken); } catch (err) { // Best-effort: log but don't fail the entire operation handleError(err, `Failed to sign out account on server:`); } }); await Promise.all(signOutPromises); // Clear all local state dispatch(clearAllAccounts()); dispatch(resetAuth()); dispatch(clearUserInUserSlice()); dispatch(baseApi.util.resetApiState()); return; } catch (error) { handleError(error, "Failed to sign out all accounts:"); return rejectWithValue(error instanceof Error ? error.message : "Unknown error"); } finally { dispatch(setAuthenticating(false)); } }); // Initialize auth - handles the startup flow export const initializeAuthThunk = createAsyncThunk("auth/initialize", async (data, { dispatch }) => { try { // Step 1: If we have a signed token, verify external user if (data.signedToken) { await dispatch(verifyExternalUserThunk({ projectId: data.projectId, userJwt: data.signedToken, })); } // Step 2: Try to refresh access token await dispatch(requestNewAccessTokenThunk({ projectId: data.projectId })); } catch (error) { handleError(error, "Auth initialization failed:"); } finally { dispatch(setInitialized(true)); } }); //# sourceMappingURL=authThunks.js.map