@replyke/core
Version:
Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.
285 lines • 11.9 kB
JavaScript
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