bug reg
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState} from "react";
|
||||
import { useEffect, useState, type SubmitEvent } from "react";
|
||||
import { useSearchParams, Link } from "react-router-dom";
|
||||
import * as React from "react";
|
||||
|
||||
function saveTokensAndRedirect(token: string, refreshToken: string, redirect: string) {
|
||||
localStorage.setItem("token", token);
|
||||
@@ -26,7 +25,7 @@ export default function LoginPage() {
|
||||
}
|
||||
}, [searchParams, redirect]);
|
||||
|
||||
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
|
||||
async function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, FormEvent } from "react";
|
||||
import { useState, type SubmitEvent } from "react";
|
||||
import { useSearchParams, Link } from "react-router-dom";
|
||||
|
||||
function saveTokensAndRedirect(token: string, refreshToken: string, redirect: string) {
|
||||
@@ -18,7 +18,7 @@ export default function RegisterPage() {
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
async function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function RegisterPage() {
|
||||
const res = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, email, password }),
|
||||
body: JSON.stringify({ username, email, password, confirmPassword }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success && data.payload?.token) {
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
//import { addLoadedFiles } from "./slices/details-slice";
|
||||
import fetchUserProfile from "./fetch-async/fetchUserProfile";
|
||||
import {
|
||||
BASE_URL,
|
||||
METHOD_POST_QUERY,
|
||||
PREFIX_DOCUMENT,
|
||||
PREFIX_UPLOAD_STREAM,
|
||||
FORM_DATA_FILES,
|
||||
DEFAULT_ERROR_STATUS,
|
||||
PROGRESS_STATUS_COMPLETED,
|
||||
PROGRESS_STATUS_ERROR,
|
||||
ERROR_NO_RESPONSE,
|
||||
UNKNOWN_ERROR,
|
||||
MAX_FILES_TO_UPLOAD,
|
||||
} from "./constants";
|
||||
|
||||
const initialState = {
|
||||
uploading: false,
|
||||
error: {
|
||||
status: null,
|
||||
message: "",
|
||||
},
|
||||
success: false,
|
||||
areFilesValid: true,
|
||||
quotaError: null,
|
||||
uploadedFiles: [],
|
||||
progress: {
|
||||
percent: 0,
|
||||
processedFiles: 0,
|
||||
totalFiles: 0,
|
||||
currentFile: "",
|
||||
status: "",
|
||||
},
|
||||
};
|
||||
|
||||
// Store AbortController outside Redux state
|
||||
let currentAbortController = null;
|
||||
|
||||
const uploadFilesSlice = createSlice({
|
||||
name: "uploadFiles",
|
||||
initialState,
|
||||
reducers: {
|
||||
resetUploadState() {
|
||||
// Abort any ongoing upload when resetting
|
||||
if (currentAbortController) {
|
||||
currentAbortController.abort();
|
||||
currentAbortController = null;
|
||||
}
|
||||
return initialState;
|
||||
},
|
||||
setAreFilesValid(state, action) {
|
||||
state.areFilesValid = action.payload;
|
||||
},
|
||||
setQuotaError(state, action) {
|
||||
state.quotaError = action.payload;
|
||||
},
|
||||
clearQuotaError(state) {
|
||||
state.quotaError = null;
|
||||
},
|
||||
uploadStarted(state, action) {
|
||||
state.uploading = true;
|
||||
state.error = { status: null, message: "" };
|
||||
state.success = false;
|
||||
state.quotaError = null;
|
||||
state.progress = {
|
||||
percent: 0,
|
||||
processedFiles: 0,
|
||||
totalFiles: action.payload.totalFiles,
|
||||
currentFile: "",
|
||||
status: "starting",
|
||||
};
|
||||
},
|
||||
progressUpdated(state, action) {
|
||||
state.progress = action.payload;
|
||||
},
|
||||
uploadCompleted(state, action) {
|
||||
state.uploading = false;
|
||||
state.success = true;
|
||||
state.uploadedFiles = action.payload.fileNames;
|
||||
},
|
||||
uploadFailed(state, action) {
|
||||
state.uploading = false;
|
||||
state.success = false;
|
||||
state.error = {
|
||||
status: action.payload.status ?? DEFAULT_ERROR_STATUS,
|
||||
message: action.payload.message ?? UNKNOWN_ERROR,
|
||||
};
|
||||
},
|
||||
uploadCancelled(state) {
|
||||
state.uploading = false;
|
||||
state.success = false;
|
||||
state.error = { status: null, message: "" };
|
||||
state.progress = initialState.progress;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
resetUploadState,
|
||||
setAreFilesValid,
|
||||
setQuotaError,
|
||||
clearQuotaError,
|
||||
uploadStarted,
|
||||
progressUpdated,
|
||||
uploadCompleted,
|
||||
uploadFailed,
|
||||
uploadCancelled,
|
||||
} = uploadFilesSlice.actions;
|
||||
|
||||
export const cancelUpload = () => (dispatch) => {
|
||||
if (currentAbortController) {
|
||||
currentAbortController.abort();
|
||||
currentAbortController = null;
|
||||
}
|
||||
dispatch(uploadCancelled());
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate files against quota limits
|
||||
* Returns { valid: boolean, error: string | null }
|
||||
*/
|
||||
export const validateFilesQuota = (files) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const loadedFiles = state.userDetails?.loadedFiles || [];
|
||||
const maxFilesToLoad = state.userDetails?.maxFilesToLoad || 0;
|
||||
|
||||
const loadedCount = loadedFiles.length;
|
||||
const remainingSlots = Math.max(0, maxFilesToLoad - loadedCount);
|
||||
const filesCount = files.length;
|
||||
|
||||
// Check max files per upload
|
||||
if (filesCount > MAX_FILES_TO_UPLOAD) {
|
||||
const errorMsg = `You can upload maximum ${MAX_FILES_TO_UPLOAD} files at once`;
|
||||
dispatch(setQuotaError(errorMsg));
|
||||
dispatch(setAreFilesValid(false));
|
||||
return { valid: false, error: errorMsg };
|
||||
}
|
||||
|
||||
// Check remaining slots
|
||||
if (filesCount > remainingSlots) {
|
||||
const errorMsg =
|
||||
remainingSlots === 0
|
||||
? `You have reached the maximum number of files (${maxFilesToLoad})`
|
||||
: `You can only upload ${remainingSlots} more file(s). Already loaded: ${loadedCount}/${maxFilesToLoad}`;
|
||||
dispatch(setQuotaError(errorMsg));
|
||||
dispatch(setAreFilesValid(false));
|
||||
return { valid: false, error: errorMsg };
|
||||
}
|
||||
|
||||
dispatch(clearQuotaError());
|
||||
dispatch(setAreFilesValid(true));
|
||||
return { valid: true, error: null };
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user can upload files (has remaining slots)
|
||||
*/
|
||||
export const canUploadFiles = () => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const loadedFiles = state.userDetails?.loadedFiles || [];
|
||||
const maxFilesToLoad = state.userDetails?.maxFilesToLoad || 0;
|
||||
|
||||
const remainingSlots = Math.max(0, maxFilesToLoad - loadedFiles.length);
|
||||
return remainingSlots > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get remaining upload slots
|
||||
*/
|
||||
export const getRemainingSlots = () => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const loadedFiles = state.userDetails?.loadedFiles || [];
|
||||
const maxFilesToLoad = state.userDetails?.maxFilesToLoad || 0;
|
||||
|
||||
return Math.max(0, maxFilesToLoad - loadedFiles.length);
|
||||
};
|
||||
|
||||
/**
|
||||
* Thunk for uploading files with SSE progress streaming.
|
||||
*/
|
||||
export const uploadFilesWithProgress =
|
||||
(files) => async (dispatch, getState) => {
|
||||
const filesArray = Array.from(files);
|
||||
const fileNames = filesArray.map((f) => f.name);
|
||||
|
||||
// Validate quota before upload
|
||||
const state = getState();
|
||||
const loadedFiles = state.userDetails?.loadedFiles || [];
|
||||
const maxFilesToLoad = state.userDetails?.maxFilesToLoad || 0;
|
||||
const loadedCount = loadedFiles.length;
|
||||
const remainingSlots = Math.max(0, maxFilesToLoad - loadedCount);
|
||||
|
||||
// Check max files per upload
|
||||
if (filesArray.length > MAX_FILES_TO_UPLOAD) {
|
||||
dispatch(
|
||||
uploadFailed({
|
||||
status: 400,
|
||||
message: `You can upload maximum ${MAX_FILES_TO_UPLOAD} files at once`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check remaining slots
|
||||
if (filesArray.length > remainingSlots) {
|
||||
dispatch(
|
||||
uploadFailed({
|
||||
status: 400,
|
||||
message:
|
||||
remainingSlots === 0
|
||||
? `You have reached the maximum number of files (${maxFilesToLoad})`
|
||||
: `You can only upload ${remainingSlots} more file(s). Already loaded: ${loadedCount}/${maxFilesToLoad}`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new AbortController for this upload
|
||||
currentAbortController = new AbortController();
|
||||
const signal = currentAbortController.signal;
|
||||
|
||||
dispatch(uploadStarted({ totalFiles: filesArray.length }));
|
||||
|
||||
const formData = new FormData();
|
||||
filesArray.forEach((file) => {
|
||||
formData.append(FORM_DATA_FILES, file);
|
||||
});
|
||||
|
||||
const token = state.userDetails?.token;
|
||||
|
||||
const headers = {};
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${BASE_URL}${PREFIX_DOCUMENT}${PREFIX_UPLOAD_STREAM}`,
|
||||
{
|
||||
method: METHOD_POST_QUERY,
|
||||
headers,
|
||||
body: formData,
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
dispatch(
|
||||
uploadFailed({
|
||||
status: response.status,
|
||||
message: `Server error: ${response.statusText}`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
// Check if aborted before reading
|
||||
if (signal.aborted) {
|
||||
reader.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("data:")) {
|
||||
const jsonStr = line.slice(5).trim();
|
||||
if (jsonStr && jsonStr.startsWith("{")) {
|
||||
try {
|
||||
const progressData = JSON.parse(jsonStr);
|
||||
dispatch(progressUpdated(progressData));
|
||||
|
||||
if (progressData.status === PROGRESS_STATUS_COMPLETED) {
|
||||
dispatch(uploadCompleted({ fileNames }));
|
||||
dispatch(fetchUserProfile());
|
||||
//dispatch(addLoadedFiles(fileNames));
|
||||
} else if (progressData.status === PROGRESS_STATUS_ERROR) {
|
||||
dispatch(
|
||||
uploadFailed({
|
||||
status: DEFAULT_ERROR_STATUS,
|
||||
message: `Error processing file: ${progressData.currentFile}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error("Failed to parse SSE data:", parseError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure completion if stream ended without explicit completed status
|
||||
const currentState = getState();
|
||||
if (currentState.uploadFiles.uploading) {
|
||||
dispatch(uploadCompleted({ fileNames }));
|
||||
dispatch(addLoadedFiles(fileNames));
|
||||
}
|
||||
} catch (err) {
|
||||
// Don't dispatch error if it was an intentional abort
|
||||
if (err.name === "AbortError") {
|
||||
console.log("Upload cancelled by user");
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Upload stream error:", err);
|
||||
dispatch(
|
||||
uploadFailed({
|
||||
status: DEFAULT_ERROR_STATUS,
|
||||
message: `${ERROR_NO_RESPONSE}: ${err.message}`,
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
currentAbortController = null;
|
||||
}
|
||||
};
|
||||
|
||||
//export default uploadFilesSlice.reducer;
|
||||
@@ -1,199 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import fetchLoginUser from "./fetch-async/fetchLoginUser";
|
||||
import fetchRegisterUser from "./fetch-async/fetchRegisterUser";
|
||||
import fetchRefreshToken from "./fetch-async/fetchRefreshToken";
|
||||
import fetchUserProfile from "./fetch-async/fetchUserProfile";
|
||||
import fetchDeleteUploaded from "./fetch-async/fetchDeleteUploaded";
|
||||
|
||||
import {
|
||||
USER_NAME_UNDEFINED,
|
||||
USER_EMAIL_UNDEFINED,
|
||||
JWT_TOKEN,
|
||||
JWT_REFRESH_TOKEN,
|
||||
TYPE_WINDOW_UNDEFINED,
|
||||
TOKEN_UNDEFINED,
|
||||
UNKNOWN_ERROR,
|
||||
} from "./constants";
|
||||
|
||||
const getInitialToken = () => {
|
||||
if (typeof window === TYPE_WINDOW_UNDEFINED) return null;
|
||||
|
||||
const token = localStorage.getItem(JWT_TOKEN);
|
||||
if (!token || token === TOKEN_UNDEFINED) return null;
|
||||
return token;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
userId: null,
|
||||
userName: USER_NAME_UNDEFINED,
|
||||
userEmail: USER_EMAIL_UNDEFINED,
|
||||
loadedFiles: [],
|
||||
maxFilesToLoad: 0,
|
||||
token: getInitialToken(),
|
||||
refreshToken: localStorage.getItem(JWT_REFRESH_TOKEN),
|
||||
loading: false,
|
||||
deleting: false,
|
||||
deleteSuccess: false,
|
||||
deletedCount: 0,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const userDetailsSlice = createSlice({
|
||||
name: "userDetails",
|
||||
initialState,
|
||||
reducers: {
|
||||
logoutUser(state) {
|
||||
state.userId = null;
|
||||
state.userName = USER_NAME_UNDEFINED;
|
||||
state.userEmail = USER_EMAIL_UNDEFINED;
|
||||
state.loadedFiles = [];
|
||||
state.maxFilesToLoad = 0;
|
||||
state.token = null;
|
||||
state.refreshToken = null;
|
||||
state.loading = false;
|
||||
state.deleting = false;
|
||||
state.deleteSuccess = false;
|
||||
state.deletedCount = 0;
|
||||
state.error = null;
|
||||
localStorage.removeItem(JWT_TOKEN);
|
||||
localStorage.removeItem(JWT_REFRESH_TOKEN);
|
||||
},
|
||||
clearError(state) {
|
||||
state.error = null;
|
||||
},
|
||||
clearDeleteStatus(state) {
|
||||
state.deleteSuccess = false;
|
||||
state.deletedCount = 0;
|
||||
},
|
||||
addLoadedFiles(state, action) {
|
||||
state.loadedFiles = [...state.loadedFiles, ...action.payload];
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(fetchRegisterUser.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchRegisterUser.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
state.userName = action.payload.payload.username;
|
||||
state.userEmail = action.payload.payload.email;
|
||||
const token = action.payload.payload.token;
|
||||
state.token = token || null;
|
||||
if (token) {
|
||||
localStorage.setItem(JWT_TOKEN, token);
|
||||
} else {
|
||||
localStorage.removeItem(JWT_TOKEN);
|
||||
}
|
||||
const refreshToken = action.payload.payload.refreshToken;
|
||||
state.refreshToken = refreshToken || null;
|
||||
if (refreshToken) {
|
||||
localStorage.setItem(JWT_REFRESH_TOKEN, refreshToken);
|
||||
} else {
|
||||
localStorage.removeItem(JWT_REFRESH_TOKEN);
|
||||
}
|
||||
});
|
||||
builder.addCase(fetchRegisterUser.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload?.status ?? 500;
|
||||
});
|
||||
|
||||
builder.addCase(fetchLoginUser.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchLoginUser.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
state.userName = action.payload.payload.username;
|
||||
state.userEmail = action.payload.payload.email;
|
||||
|
||||
const token = action.payload.payload.token;
|
||||
state.token = token || null;
|
||||
if (token) {
|
||||
localStorage.setItem(JWT_TOKEN, token);
|
||||
} else {
|
||||
localStorage.removeItem(JWT_TOKEN);
|
||||
}
|
||||
|
||||
const refreshToken = action.payload.payload.refreshToken;
|
||||
state.refreshToken = refreshToken || null;
|
||||
if (refreshToken) {
|
||||
localStorage.setItem(JWT_REFRESH_TOKEN, refreshToken);
|
||||
} else {
|
||||
localStorage.removeItem(JWT_REFRESH_TOKEN);
|
||||
}
|
||||
});
|
||||
builder.addCase(fetchLoginUser.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload ?? { status: 500, message: "Unknown error" };
|
||||
});
|
||||
|
||||
builder.addCase(fetchRefreshToken.fulfilled, (state, action) => {
|
||||
const token = action.payload?.payload?.token;
|
||||
const refreshToken = action.payload?.payload?.refreshToken;
|
||||
|
||||
state.token = token || null;
|
||||
state.refreshToken = refreshToken || null;
|
||||
|
||||
if (token) localStorage.setItem(JWT_TOKEN, token);
|
||||
else localStorage.removeItem(JWT_TOKEN);
|
||||
|
||||
if (refreshToken) localStorage.setItem(JWT_REFRESH_TOKEN, refreshToken);
|
||||
else localStorage.removeItem(JWT_REFRESH_TOKEN);
|
||||
});
|
||||
|
||||
builder.addCase(fetchRefreshToken.rejected, (state, action) => {
|
||||
state.error = action.payload ?? { status: 500, message: UNKNOWN_ERROR };
|
||||
});
|
||||
|
||||
builder.addCase(fetchUserProfile.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
});
|
||||
|
||||
builder.addCase(fetchUserProfile.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
|
||||
const p = action.payload?.payload;
|
||||
|
||||
if (p?.id) state.userId = p.id;
|
||||
if (p?.username) state.userName = p.username;
|
||||
if (p?.email) state.userEmail = p.email;
|
||||
if (p?.loadedFiles) state.loadedFiles = p.loadedFiles;
|
||||
if (p?.maxLoadedFiles != null) state.maxFilesToLoad = p.maxLoadedFiles;
|
||||
});
|
||||
|
||||
builder.addCase(fetchUserProfile.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload ?? { status: 500, message: "Unknown error" };
|
||||
});
|
||||
|
||||
// Delete uploaded files
|
||||
builder.addCase(fetchDeleteUploaded.pending, (state) => {
|
||||
state.deleting = true;
|
||||
state.deleteSuccess = false;
|
||||
state.deletedCount = 0;
|
||||
state.error = null;
|
||||
});
|
||||
|
||||
builder.addCase(fetchDeleteUploaded.fulfilled, (state, action) => {
|
||||
state.deleting = false;
|
||||
state.deleteSuccess = true;
|
||||
state.deletedCount = action.payload?.payload ?? 0;
|
||||
state.loadedFiles = [];
|
||||
state.error = null;
|
||||
});
|
||||
|
||||
builder.addCase(fetchDeleteUploaded.rejected, (state, action) => {
|
||||
state.deleting = false;
|
||||
state.deleteSuccess = false;
|
||||
state.error = action.payload ?? { status: 500, message: "Unknown error" };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
//export const { logoutUser, clearError, clearDeleteStatus, addLoadedFiles } = userDetailsSlice.actions;
|
||||
//export default userDetailsSlice.reducer;
|
||||
Reference in New Issue
Block a user