I am building an app in NextJS which uses Firebase authentication. After successful authentication, I then wish to fetch additional details about the customer stored within a MongoDB instance (or create a new document for the customer on first login). Because I cannot access the firebase auth object inside getServerSideProps, I have redirected after firebase.auth() to /dashboard/${user.uid} which then getsServerSideProps using the uid passed to fetch customized/dynamic content.
dashboard.js
export default function DashboardAuth(props) {
const [user, loading, error] = useAuthState(firebase.auth())
if (user){
return window.location.href = `/dashboard/${user.uid}`
} else {
return <SignIn/>
}
}
/dashboard/[id].js
export async function getServerSideProps({ params }) {
let userData
console.log("Logging in ")
const { db } = await connectToDatabase();
console.log("connected to database, awaiting query response with uid")
const findUserResp = await db
.collection("users")
.findOne({'uid': params.id})
if(findUserResp ){
console.log("user data exists")
userData = {
uid: findUserResp.uid,
email: findUserResp.email,
displayName: findUserResp.displayName,
photoURL: findUserResp.photoURL,
storageQuoteRemaining: findUserResp.storageQuoteRemaining,
emailVerified: findUserResp.emailVerified,
currentPage: '/dashboard'
}
}else{
console.log("user data does not exist")
userData = {
uid:params.id,
email: '',
displayName: '',
photoURL: '',
storageQuoteRemaining: 0,
emailVerified: false,
currentPage: '/dashboard'
}
const addUserResp = await db
.collection("users")
.insertOne(userData)
}
console.log("returning userdata below")
console.log(userData)
return {
props: {
userData
}
}
}
export default function Dashboard(props) {
const [user, loading, error] = useAuthState(firebase.auth())
const userContext = getUserContext()
useEffect(() => {
userContext.handleCurrentUser(props.userData)
}, []);
if (user && props.userData.uid === user.uid){
return <Layout children={<CreateItem/>}/>
}else{
return <SignIn/>
}
}
My main issue is that after the user is initially added to mongodb on first login, immediatley after redirect to [id].js, I am presented with an error
Error: Error serializing `.userData._id` returned from `getServerSideProps` in "/dashboard/[id]".
Reason: `object` ("[object Object]") cannot be serialized as JSON. Please only return JSON serializable data types.
but on refresh this disappears.
Also I don't like how I have written my redirect but useRouter does not work. Any advice on how to better do this would be appreciated.
Looks like your first issue is related to what is being said here - https://github.com/vercel/next.js/issues/11993#issuecomment-617375501. The solve being to:
JSON.parse(JSON.stringify(findUserResp)) which is the data returned.
You should do a middleware solve (NextJS 12) as explained here or a redirect key inside your getServerSeideProps if using older than NextJS 12
if (user) {
return {
redirect: {
destination: '/dashboard/${user.uid}',
permanent: false,
},
}
Related
I'm trying to implement User Authentication in a Remix app and have been going around in circles trying to figure out why destroying the session is returning [object Promise] instead of clearing it.
My auth.server.js file contains functions that manage anything relating to creating, retrieving and destroying the Session Cookie, as well as a function to login.
import { hash, compare } from "bcryptjs";
import { prisma } from "./database.server";
import { createCookieSessionStorage, redirect } from "#remix-run/node";
export const sessionStorage = createCookieSessionStorage({
cookie: {
secure: process.env.NODE_ENV === "production",
path: "/",
secrets: [process.env.SESSION_SECRET],
sameSite: "lax",
maxAge: 30 * 24 * 60 * 60, // 30 days
httpOnly: true
}
});
async function createUserSession(userId, redirectPath) {
const session = await sessionStorage.getSession();
session.set("userId", userId);
return redirect(redirectPath, {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session)
}
});
}
export async function getUserFromSession(request) {
const session = await sessionStorage.getSession(
request.headers.get("Cookie")
);
const userId = session.get("userId");
return userId ? userId : null;
}
export function destroyUserSession(request) {
const session = sessionStorage.getSession(request.headers.get("Cookie"));
return redirect("/", {
headers: {
"Set-Cookie": sessionStorage.destroySession(session)
}
});
}
export async function login({ email, password }) {
const existingUser = await prisma.user.findFirst({ where: { email } });
if (!existingUser) {
return throwError(
"Could not log you in, please check provided email.",
401
);
}
const passwordCorrect = await compare(password, existingUser.password);
if (!passwordCorrect) {
return throwError(
"Could not log you in, please check provided password.",
401
);
}
return createUserSession(existingUser.id, "/");
}
function throwError(text, code) {
const error = new Error(text);
error.status = code; // authentication error code
throw error; // this triggers ErrorBoundary, as it's not an error response
}
My logout function is in a separate .js file located inside the /routes folder.
import { json } from "#remix-run/node";
import { destroyUserSession } from "~/data/auth.server";
export function action({ request }) {
if (request.method !== "POST") {
throw json({ message: "Invalid request method" }, { status: 400 });
}
return destroyUserSession(request);
}
Finally, the Logout button is inside the navigation bar and contained within a <Form> element.
<Form method="post" action="/logout">
<button type="submit">Logout</button>
</Form>
SOLVED
I was somehow able to solve it by moving the logout function into auth.server.js and leaving the following in the logout.js route:
import { redirect } from "#remix-run/node";
import { logout } from "~/data/auth.server";
export const action = ({ request }) => logout(request);
export const loader = () => redirect("/auth");
The session functions are all async. The reason you had [object Promise] was that you were returning the promise directly. You need to use await.
export function destroyUserSession(request) {
const session = sessionStorage.getSession(request.headers.get("Cookie"));
return redirect("/", {
headers: {
// use await on session functions
"Set-Cookie": await sessionStorage.destroySession(session)
}
});
}
P.S. That's one of the benefits of TypeScript. It would have told you that headers required a string value and would not allow the Promise return.
I've been working on the user registration of my React-Redux project (I'm really new to Redux), and I'm having trouble with registering Users with error handling...
My curriculum teaches nothing on using Redux with React (which is what my project does) to send data to an backend database.
I have been using useState for setting both the current user (currentUser, onLogin) and any errors (errors, setErrors) inside of UserInput.jsx, and I'm trying to move that functionality over to usersSlice.js.
Right now, I am trying to set my User object to be the action.payload. I also need to deal with how to pass the new User's info from my form, to my Slice.
Any solutions?
My code:
usersSlice.js
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
export const signup = createAsyncThunk("users/signup", async ({username, password}, thunkAPI) => {
await fetch("/signup", {
method: "POST",
headers: headers,
body: JSON.stringify({user: {username, password}})
}).then((data) => {
data.json().then((data) => {
if(data.errors){
return thunkAPI.rejectWithValue(data.errors);
} else{
// Find a way to set the current user!!!
return data;
}
})
})
});
const usersSlice = createSlice({
name: "users",
initialState: {
user: [], // This should be an SINGLE User Object!!!
errorMessage: null,
status: 'idle',
},
reducers: {
userLogin(state, action){
state.user.push(action.payload);
},
userLogout(state){
state.user = [];
},
},
extraReducers(builder){
// Omit extraReduxers logic
}
});
UserInput.jsx:
function UserInput({onLogin, username, setUsername, password, setPassword, errors, setErrors}){
const dispatch = useDispatch();
function handleSubmit(e){
e.preventDefault();
const user ={
username: username,
password: password
}
dispatch(signup(user));
if(user.errors) { // Returns null due to the user object above...
setErrors(user.errors);
}
else{
setErrors(null);
onLogin(user); // setCurrentUser(user);
}
}
// Omit the signup form
}
export default UserInput;
I'm trying to fetch an array of objects from MongoDB, using mongoose and SSP. One hitch is that all ObjectIds must be converted into strings. Currently I'm doing it like this:
export async function getServerSideProps({ query }) {
try {
const { user } = query
await connectDB()
const currentUser = await User.findOne({ user }).lean(),
{ _id } = await currentUser,
userProperties = await Property.find({ ownerId: _id }).lean()
currentUser._id = currentUser._id.toString()
userProperties.forEach(props => {
props._id = props._id.toString()
props.ownerId = props.ownerId.toString()
props.subarray.forEach(props => {
props._id = props._id.toString()
})
})
if (!currentUser) {
return {
notFound: true
}
}
return {
props: {
currentUser,
userProperties
}
}
} catch (err) {
console.log(err)
return {
redirect: {
destination: '/',
statusCode: 307
}
}
}
}
This produces: Error: If(...): Nothing was returned from render. I can fetch user without properties, no propblem, and I can console log the attached properties even though nothing is returned. What's happening here?
In order to send an array from getServerSideProps(), you should first transform it to a series of bytes with JSON.stringify, remember that HTTP sends text only. Re-transform as an array of objects in your React component with JSON.parse, with this method, you don't need to create the strings manually.
The question stands as-is - how to implement cookie authentication in a SvelteKit & MongoDB app? Meaning how to properly use hooks, endpoints, establish a DB connection and show it on a boilerplate-ish project.
After SvelteKit Project Initialisation
#1 Install additional dependencies
npm install config cookie uuid string-hash mongodb
I prefer config over vite's .env variables due to all the leaks and problems regarding it
cookie is used to properly set cookies
uuid is used to generate complex cookie IDs
string-hash is a simple yet secure hashing for passwords stored in your DB
mongodb is used to establish a connection to your DB
#2 Set up config
In root, create a folder called config. Inside it, create a file called default.json.
config/default.json
{
"mongoURI": "<yourMongoURI>",
"mongoDB": "<yourDatabaseName>"
}
#3 Set up base DB connection code
Create lib folder in src. Inside it, create db.js file.
src/lib/db.js
import { MongoClient } from 'mongodb';
import config from 'config';
export const MONGODB_URI = config.get('mongoURI');
export const MONGODB_DB = config.get('mongoDB');
if (!MONGODB_URI) {
throw new Error('Please define the mongoURI property inside config/default.json');
}
if (!MONGODB_DB) {
throw new Error('Please define the mongoDB property inside config/default.json');
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*/
let cached = global.mongo;
if (!cached) {
cached = global.mongo = { conn: null, promise: null };
}
export const connectToDatabase = async () => {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
useNewUrlParser: true,
useUnifiedTopology: true
};
cached.promise = MongoClient.connect(MONGODB_URI, opts).then((client) => {
return {
client,
db: client.db(MONGODB_DB)
};
});
}
cached.conn = await cached.promise;
return cached.conn;
}
The code is taken from next.js implementation of MongoDB connection establishment and modified to use config instead of .env.
#4 Create hooks.js file inside src
src/hooks.js
import * as cookie from 'cookie';
import { connectToDatabase } from '$lib/db';
// Sets context in endpoints
// Try console logging context in your endpoints' HTTP methods to understand the structure
export const handle = async ({ request, resolve }) => {
// Connecting to DB
// All database code can only run inside async functions as it uses await
const dbConnection = await connectToDatabase();
const db = dbConnection.db;
// Getting cookies from request headers - all requests have cookies on them
const cookies = cookie.parse(request.headers.cookie || '');
request.locals.user = cookies;
// If there are no cookies, the user is not authenticated
if (!cookies.session_id) {
request.locals.user.authenticated = false;
}
// Searching DB for the user with the right cookie
// All database code can only run inside async functions as it uses await
const userSession = await db.collection('cookies').findOne({ cookieId: cookies.session_id });
// If there is that user, authenticate him and pass his email to context
if (userSession) {
request.locals.user.authenticated = true;
request.locals.user.email = userSession.email;
} else {
request.locals.user.authenticated = false;
}
const response = await resolve(request);
return {
...response,
headers: {
...response.headers
// You can add custom headers here
// 'x-custom-header': 'potato'
}
};
};
// Sets session on client-side
// try console logging session in routes' load({ session }) functions
export const getSession = async (request) => {
// Pass cookie with authenticated & email properties to session
return request.locals.user
? {
user: {
authenticated: true,
email: request.locals.user.email
}
}
: {};
};
Hooks authenticate the user based on cookies and pass the desired variables (in this example it is the user's email etc.) to context & session.
#5 Create register.js & login.js Endpoints inside auth folder
src/routes/auth/register.js
import stringHash from 'string-hash';
import * as cookie from 'cookie';
import { v4 as uuidv4 } from 'uuid';
import { connectToDatabase } from '$lib/db';
export const post = async ({ body }) => {
// Connecting to DB
// All database code can only run inside async functions as it uses await
const dbConnection = await connectToDatabase();
const db = dbConnection.db;
// Is there a user with such an email?
const user = await db.collection('testUsers').findOne({ email: body.email });
// If there is, either send status 409 Conflict and inform the user that their email is already taken
// or send status 202 or 204 and tell them to double-check on their credentials and try again - it is considered more secure
if (user) {
return {
status: 409,
body: {
message: 'User with that email already exists'
}
};
}
// Add user to DB
// All database code can only run inside async functions as it uses await
await db.collection('testUsers').insertOne({
name: body.name,
email: body.email,
password: stringHash(body.password)
});
// Add cookie with user's email to DB
// All database code can only run inside async functions as it uses await
const cookieId = uuidv4();
await db.collection('cookies').insertOne({
cookieId,
email: body.email
});
// Set cookie
// If you want cookies to be passed alongside user when they redirect to another website using a link, change sameSite to 'lax'
// If you don't want cookies to be valid everywhere in your app, modify the path property accordingly
const headers = {
'Set-Cookie': cookie.serialize('session_id', cookieId, {
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: 'strict',
path: '/'
})
};
return {
status: 200,
headers,
body: {
message: 'Success'
}
};
};
If you want to take it a step further, don't forget to create Schemas with Mongoose!
src/routes/auth/login.js
import stringHash from 'string-hash';
import * as cookie from 'cookie';
import { v4 as uuidv4 } from 'uuid';
import { connectToDatabase } from '$lib/db';
export const post = async ({ body }) => {
const dbConnection = await connectToDatabase();
const db = dbConnection.db;
const user = await db.collection('testUsers').findOne({ email: body.email });
if (!user) {
return {
status: 401,
body: {
message: 'Incorrect email or password'
}
};
}
if (user.password !== stringHash(body.password)) {
return {
status: 401,
body: {
message: 'Unauthorized'
}
};
}
const cookieId = uuidv4();
// Look for existing email to avoid duplicate entries
const duplicateUser = await db.collection('cookies').findOne({ email: body.email });
// If there is user with cookie, update the cookie, otherwise create a new DB entry
if (duplicateUser) {
await db.collection('cookies').updateOne({ email: body.email }, { $set: { cookieId } });
} else {
await db.collection('cookies').insertOne({
cookieId,
email: body.email
});
}
// Set cookie
const headers = {
'Set-Cookie': cookie.serialize('session_id', cookieId, {
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: 'strict',
path: '/'
})
};
return {
status: 200,
headers,
body: {
message: 'Success'
}
};
};
#6 Create Register.svelte and Login.svelte components
src/lib/Register.svelte
<script>
import { createEventDispatcher } from 'svelte';
// Dispatcher for future usage in /index.svelte
const dispatch = createEventDispatcher();
// Variables bound to respective inputs via bind:value
let email;
let password;
let name;
let error;
const register = async () => {
// Reset error from previous failed attempts
error = undefined;
try {
// POST method to src/routes/auth/register.js endpoint
const res = await fetch('/auth/register', {
method: 'POST',
body: JSON.stringify({
email,
password,
name
}),
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
dispatch('success');
} else {
error = 'An error occured';
}
} catch (err) {
console.log(err);
error = 'An error occured';
}
};
</script>
<h1>Register</h1>
<input type="text" name="name" placeholder="Enter your name" bind:value={name} />
<input type="email" name="email" placeholder="Enter your email" bind:value={email} />
<input type="password" name="password" placeholder="Enter your password" bind:value={password} />
{#if error}
<p>{error}</p>
{/if}
<button on:click={register}>Register</button>
src/lib/Login.svelte
<script>
import { createEventDispatcher } from 'svelte';
// Dispatcher for future usage in /index.svelte
const dispatch = createEventDispatcher();
// Variables bound to respective inputs via bind:value
let email;
let password;
let error;
const login = async () => {
// Reset error from previous failed attempts
error = undefined;
// POST method to src/routes/auth/login.js endpoint
try {
const res = await fetch('/auth/login', {
method: 'POST',
body: JSON.stringify({
email,
password
}),
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
dispatch('success');
} else {
error = 'An error occured';
}
} catch (err) {
console.log(err);
error = 'An error occured';
}
};
</script>
<h1>Login</h1>
<input type="email" name="email" placeholder="Enter your email" bind:value={email} />
<input type="password" name="password" placeholder="Enter your password" bind:value={password} />
{#if error}
<p>{error}</p>
{/if}
<button on:click={login}>Login</button>
#7 Update src/routes/index.svelte
src/routes/index.svelte
<script>
import Login from '$lib/Login.svelte';
import Register from '$lib/Register.svelte';
import { goto } from '$app/navigation';
// Redirection to /profile
function redirectToProfile() {
goto('/profile');
}
</script>
<main>
<h1>Auth with cookies</h1>
<!-- on:success listens for dispatched 'success' events -->
<Login on:success={redirectToProfile} />
<Register on:success={redirectToProfile} />
</main>
#8 Create index.svelte inside profile folder
src/routes/profile/index.svelte
<script context="module">
export async function load({ session }) {
if (!session.user.authenticated) {
return {
status: 302,
redirect: '/auth/unauthorized'
};
}
return {
props: {
email: session.user.email
}
};
}
</script>
<script>
import { onMount } from 'svelte';
export let email;
let name;
onMount(async () => {
const res = await fetch('/user');
const user = await res.json();
name = user.name;
});
</script>
<h1>Profile</h1>
<p>Hello {name} you are logged in with the email {email}</p>
Pay attention to session we set up in hooks.js. console.log() it to understand its structure better. I won't be implementing /auth/unauthorized route, so mind that.
#9 Create index.js endpoint inside user folder
src/routes/user/index.js
import { connectToDatabase } from '$lib/db';
export const get = async (context) => {
// Connecting to DB
// All database code can only run inside async functions as it uses await
const dbConnection = await connectToDatabase();
const db = dbConnection.db;
// Checking for auth coming from hooks' handle({ request, resolve })
if (!context.locals.user.authenticated) {
return {
status: 401,
body: {
message: 'Unauthorized'
}
};
}
const user = await db.collection('testUsers').findOne({ email: context.locals.user.email });
if (!user) {
return {
status: 404,
body: {
message: 'User not found'
}
};
}
// Find a proper way in findOne(), I've run out of gas ;)
delete user.password;
return {
status: 200,
body: user
};
};
Final thoughts
There are almost none tutorials regarding SvelteKit and I'll surely find this guide useful in my future projects. If you find a bug or see an improvement, feel free to let me know so I can make this guide better ;)
Big thanks to Brayden Girard for a precedent for this guide!
https://www.youtube.com/channel/UCGl66MHcjMDJyIPZkuKULSQ
Happy coding!
I'm using nextjs and apollo (with react hooks). I am trying to update the user object in the apollo cache (I don't want to refetch). What is happening is that the user seems to be getting updated in the cache just fine but the user object that the component uses is not getting updated. Here is the relevant code:
The page:
// pages/index.js
...
const Page = ({ user }) => {
return <MyPage user={user} />;
};
Page.getInitialProps = async (context) => {
const { apolloClient } = context;
const user = await apolloClient.query({ query: GetUser }).then(({ data: { user } }) => user);
return { user };
};
export default Page;
And the component:
// components/MyPage.jsx
...
export default ({ user }) => {
const [toggleActive] = useMutation(ToggleActive, {
variables: { id: user.id },
update: proxy => {
const currentData = proxy.readQuery({ query: GetUser });
if (!currentData || !currentData.user) {
return;
}
console.log('user active in update:', currentData.user.isActive);
proxy.writeQuery({
query: GetUser,
data: {
...currentData,
user: {
...currentData.user,
isActive: !currentData.user.isActive
}
}
});
}
});
console.log('user active status:', user.isActive);
return <button onClick={toggleActive}>Toggle active</button>;
};
When I continuously press the button, the console log in the update function shows the user active status as flipping back and forth, so it seems that the apollo cache is getting updated properly. However, the console log in the component always shows the same status value.
I don't see this problem happening with any other apollo cache updates that I'm doing where the data object that the component uses is acquired in the component using the useQuery hook (i.e. not from a query in getInitialProps).