I am using iron-session, next-connect with nextjs in our webapp and one of the requirements is to publish analytics events from our frontend code, like page views, button clicks and other custom events. These events are stored in our database and used by our data analyst with PowerBI.
Our webapp takes a user on an onboarding journey, then once it's done, we create an account for the user and redirects to dashboard. For the onboarding part, we don't have a user id yet while in the dashboard, we already do. However, we want to be able to track the user journey in the webapp so we need an identifier that is persisted throughout the whole journey. Thus, we think of a session id with the iron-session.
Now iron-session doesn't have a concept of session id, so I am trying to implement it myself. The session id will be our identifier of the user in our events table.
Here is the withSession middleware used with next-connect
import { getIronSession } from "iron-session";
import type { IncomingMessage } from "http";
import type { NextApiRequest } from "next";
import { nanoid } from "nanoid";
import appConfig from "#/backend/app.config";
export const sessionOptions = {
password: appConfig.secret,
cookieName: appConfig.cookies.sessionToken.name,
cookieOptions: appConfig.cookies.sessionToken.options,
};
export async function withSession(
req: IncomingMessage | NextApiRequest,
res: any,
next: any
) {
const session = await getIronSession(req, res, sessionOptions);
if (!session.id) session.id = nanoid(32);
req.session = session;
await req.session.save();
return next();
}
declare module "iron-session" {
interface IronSessionData {
user?: { id: string };
id: string;
}
}
And a route that will use the middleware
const router = createRouter<NextApiRequest, NextApiResponse>()
.use(...([withSession, withLogger, withTenant] as const))
.get(async (req, res) => {
// Authenticate user
req.session.user = { id: userId };
await req.session.save();
return res.redirect("/");
});
export default router.handler();
Is this a correct implementation of the said requirement?
Some libraries implement a kind of session.regenerate() when a user perform signIn and signOut. Do I need to implement it too? If I do, I will lose the identifier that persists throughout the whole user journey.
since you are using typescript first define the type of session object
declare module "iron-session" {
interface IronSessionData {
nameOfSessionObject?: {
// in your implementation you were creating req.user and req.id
// you could overwrite the req properties
user?: { id: string };
// you can manually create on the server
id: string;
};
}
}
create a wrapper session function
export function withSession(handler: any) {
return withIronSessionApiRoute(handler, {
password: appConfig.secret,
cookieName: appConfig.cookies.sessionToken.name,
// Said in another way, the browser will not send a cookie with the secure attribute set over an unencrypted HTTP request
cookieOptions: appConfig.cookies.sessionToken.options,
})}
create the session object. you do not use getIronSession when creating a session.
you need that when you need to access to the session object in middleware
export default withSessio(
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "GET") {
try {
const sessionObj={....}
req.session.nameOfSessionObject={...sessionObj}
await req.session.save();
// whatever you want to return
return res.json(sessionObj);
} catch (error) {
console.error("error in verify post req", error);
// 422 Unprocessable Entity
res.status(422).send({ message: "Cannot create SESSION" });
}
} else if (req.method === "POST") {
try {
..HANDLE POST HERE
} catch (error) {
res.status(422).send({ message: "Cannot generate a SESSION" });
}
} else {
return res.status(200).json({ message: "Invalid api Route" });
}
}
);
now you can import above handler and connect with next-connect
Related
I have this code in my /api/[verificationToken] which is when accessed by the user, the verification token will be updated. For now, I am trying to check if the token exists in the database and corresponds to a registered email.
import prisma from "../../../lib/prisma";
export default async function handler(req, res) {
const token = req.query;
const findEmail = await prisma.user.findFirst({
where: {
token: token.verificationToken,
},
});
if (findEmail) {
console.log("email exists");
} else {
console.log("email doesn't exist");
return res.redirect("/auth/login");
}
}
The problem is, when I go to http://localhost:3000/auth/api/nonexistenttoken, "email doesn't exist" displays in a loop. I have also tried
res.writeHead(302, {
Location: '/auth/login'
});
res.end();
But it still gives me the same loop. What I want to happen is that when the token doesn't exist (which also means the email also doesn't), it should redirect the user to the login page.
I am attempting to get the current logged in supabase user while server side.
I have attempted to use const user = supabase.auth.user(); but I always get a null response.
I have also attempted const user = supabase.auth.getUserByCookie(req) but it also returns null. I think because I am not sending a cookie to the api when calling it from the hook.
I have tried passing the user.id from the hook to the api but the api is not receiving the parameters.
I also attempted this approach but the token is never fetched. It seems to not exist in req.cookies.
let supabase = createClient(supabaseUrl, supabaseKey);
let token = req.cookies['sb:token'];
if (!token) {
return
}
let authRequestResult = await fetch(`${supabaseUrl}/auth/v1/user`, {
headers: {
'Authorization': `Bearer ${token}`,
'APIKey': supabaseKey
}
});
`
Does anyone know how to get the current logged in user in server side code?
If you need to get the user in server-side, you need to set the Auth Cookie in the server using the given Next.js API.
// pages/api/auth.js
import { supabase } from "../path/to/supabaseClient/definition";
export default function handler(req, res) {
if (req.method === "POST") {
supabase.auth.api.setAuthCookie(req, res);
} else {
res.setHeader("Allow", ["POST"]);
res.status(405).json({
message: `Method ${req.method} not allowed`,
});
}
}
This endpoint needs to be called every time the state of the user is changed, i.e. the events SIGNED_IN and SIGNED_OUT
You can set up a useEffect in _app.js or probably in a User Context file.
// _app.js
import "../styles/globals.css";
import { supabase } from '../path/to/supabaseClient/def'
function MyApp({ Component, pageProps }) {
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
handleAuthChange(event, session)
if (event === 'SIGNED_IN') {
// TODO: Actions to Perform on Sign In
}
if (event === 'SIGNED_OUT') {
// TODO: Actions to Perform on Logout
}
})
checkUser()
return () => {
authListener.unsubscribe()
}
}, [])
return <Component {...pageProps} />;
}
async function handleAuthChange(event, session) {
await fetch('/api/auth', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'same-origin',
body: JSON.stringify({ event, session }),
})
}
export default MyApp;
You can now handle this user with a state and pass it to the app or whichever way you'd like to.
You can get the user in the server-side in any Next.js Page
// pages/user_route.js
import { supabase } from '../path/to/supabaseClient/def'
export default function UserPage ({ user }) {
return (
<h1>Email: {user.email}</h1>
)
}
export async function getServerSideProps({ req }) {
const { user } = await supabase.auth.api.getUserByCookie(req)
if (!user) {
return { props: {}, redirect: { destination: '/sign-in' } }
}
return { props: { user } }
}
Here's a YouTube Tutorial from Nader Dabit - https://www.youtube.com/watch?v=oXWImFqsQF4
And his GitHub Repository - https://github.com/dabit3/supabase-nextjs-auth
supabase have a library of helpers for managing auth for both client- and server-side auth and fetching in a couple of frameworks including Next.js: https://github.com/supabase/auth-helpers and appears to be the recommended solution for similar problems based on this thread: https://github.com/supabase/supabase/issues/3783
This is how I'm using it in an API handler, but provided you have access to req, you can access the user object this way:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies["sb-access-token"]);
Note that you will need to use the helper library supabaseClient and supabaseServerClient on the client and server side respectively for this to work as intended.
I was following a tutorial today and was having a similar issue and the below is how i managed to fix it.
I've got this package installed github.com/jshttp/cookie which is why i'm calling cookie.parse.
Supabase Instance:
`//../../../utils/supabase`
import { createClient } from "#supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_KEY
);
In my case this was my API page:
import { supabase } from "../../../utils/supabase";
import cookie from "cookie";
import initStripe from "stripe";
const handler = async (req, res) => {
const { user } = await supabase.auth.api.getUserByCookie(req);
if (!user) {
return res.status(401).send("Unathorized");
}
const token = cookie.parse(req.headers.cookie)["sb-access-token"];
supabase.auth.session = () => ({
access_token: token,
});`
const {
data: { stripe_customer },
} = await supabase
.from("profile")
.select("stripe_customer")
.eq("id", user.id)
.single();
For anyone who tries to figure out how to get the user server side with the new #supabase/auth-helpers-nextjs, Michele gave the answer.
Just a note: If you're trying to get the user on nextJs's Middleware, instead of:
... req.cookies["sb-access-token"]
You have to use: req.cookies.get('sb-access-token')
For example:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies.get('sb-access-token'))
UPDATE: 2023. Available now on Supabase Docs here
import { createServerSupabaseClient } from '#supabase/auth-helpers-nextjs'
export default function Profile({ user }) {
return <div>Hello {user.name}</div>
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createServerSupabaseClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}
I am implementing a command/response pattern where the user writes to a command collection by calling add with a payload under his own userId, and then gets the data from a similar response path.
However the code below doesn't work, because onSnapshot can not listen for a document that hasn't yet been created (document command.id in the /responses/{userId}/register collection). This would be easy to solve with an onCreate handler, which exists for cloud functions but not for the JS firebase client API it seems.
This is using redux-firestore and some of my app helper functions, but you'll get the idea. The command and response document structures use { payload, error} similar to FSA
Cloud Function
export const register = functions.firestore
.document("commands/{userId}/register/{commandId}")
.onCreate(async event => {
const payload = event.data.get("payload");
const { userId, commandId } = event.params;
const response = db.document(`responses/${userId}/register/${commandId}`)
// possibly something bad will happen
try {
// do something with payload...
return response.set({
payload: "ok" // or pass relevant response data
})
} catch(err) {
return response.set({
error: true
payload: error
})
}
});
Client
export async function register(fs: any, userId: string) {
try {
// issue a new command
const command = await fs.add(
{ collection: `commands/${userId}/register` },
{ payload: fs.firestore.FieldValue.serverTimestamp() }
);
// wait for the response to be created
fs.onSnapshot(
{ collection: `responses/${userId}/register`, doc: command.id },
function onNext(doc) {
const {error, payload} = doc.data()
if (error) {
return notify.error({ title: 'Failed to register', message: payload.message });
}
notify.json(payload);
},
function onError(err) {
notify.error(err);
}
);
} catch (err) {
notify.error(err);
}
}
Is there no such thing as onCreate for web clients?
The only scalable solution I can think of is to store the response data as a child in the command document, but I think it is not as nice, because I suspect you can not make the permissions as strict then.
I would like the user only to be able to write to the command, and only read from the response paths. If I place the response as a child of command, this would not be possible I think?
I'm wondering if I'm not overlooking some API...
i'm stuck on this issue working on a ionic2 project with "firestore" service from firebase.
I have an osservable to get some data from firestore in a template using the async pipe.
Rule on this EndPoint give read and write access only to logged user.
When i sign-out i put a redirect to login page.
..and now come the issue..
when i land in the login page, after a few second, jump out the IonicErrorHandler notifying that i have insufficient permission.
so;
how i can tell to firestore osservable;
"hey dude, stop it, i call u later if someone log-in again"
(ill try an unsubscribe() befour the signOut but not work, and also
it does not from persistence)
Recapping:
when i logOut
this.afAuth.auth.signOut();
the error:
core.es5.js:1020 ERROR Error: Missing or insufficient permissions.
at new FirestoreError (error.js:164)
at JsonProtoSerializer.fromRpcStatus (serializer.js:126)
at JsonProtoSerializer.fromWatchChange (serializer.js:517)
at PersistentListenStream.onMessage (persistent_stream.js:334)
at persistent_stream.js:270
at persistent_stream.js:247
at async_queue.js:81
at t.invoke (polyfills.js:3)
at Object.onInvoke (core.es5.js:3890)
at t.invoke (polyfills.js:3)
(to be precise, i recive it 3 times. Exactly the number or of documents in the collection)
Service where i call the firestore endpoint:
export interface Attivita {
id: string;
committente: string;
durata: number;
nome: string;
progetto: string;
userId: string;
}
#Injectable()
export class FirebaseService {
attivitaCollectionRef: AngularFirestoreCollection<Attivita>;
attivita$: Observable<Attivita[]>;
constructor(private afs: AngularFirestore,
public afAuth: AngularFireAuth ) {
}
setOsservableAttivita(uId){
this.attivitaCollectionRef = this.afs.collection('attivita', ref => {
return ref.where("userId", "==", uId)
});
this.attivita$ = this.attivitaCollectionRef.snapshotChanges().map(actions => {
return actions.map(action => {
console.log(action)
const data = action.payload.doc.data() as Attivita;
const id = action.payload.doc.id;
return { id, ...data };
});
});
}
}
tks in advance to all help me to understand it
:)
I'd recommend watching the authState from Firebase and only taking from snapshotChanges while you're authenticated. The switchMap operator allows you to switch between observables based on conditions such as whether or not the user is authenticated. Here is an example of a possible solution.
// Assuming rxjs 5.5.0 with lettable operators
import { map } from 'rxjs/operators/map';
import { switchMap } from 'rxjs/operators/switchMap';
import { empty } from 'rxjs/observable/empty';
import { create } from 'rxjs/observable/create';
const actions$ = this.attivitaCollectionRef.snapshotChanges()
.map(actions => {
return actions.map(action => {
console.log(action)
const data = action.payload.doc.data() as Attivita;
const id = action.payload.doc.id;
return { id, ...data };
});
});
}),
this.attivita$ = create(
// Listen for changes in the authState
subscriber => this.afAuth.onAuthStateChanged(subscriber))
)
.pipe(
// Determine if the user is logged in
map(user => !!user),
// switchMap will unsubscribe from the previous observable
// so when isLoggedIn switches to false actions$ will be unsubscribed from
switchMap(isLoggedIn => isLoggedIn ? actions$ : empty()),
);
calling this after logout solved it for me:
afStore.firestore.disableNetwork();
Yes,
Normally this is solved by doing an unsubscribe of your subscription in the ngOnDestroy() of the component you are navigating away from.
That's the way I do it.
So for you, that would be:
ngOnDestroy() {
this.attivita$.unsubscribe();
}
However, it is very difficult to pinpoint which you should unsubscribe as the error does not give any indication on that.
I have added a question to the devs of angularFire on your issue:
https://github.com/angular/angularfire2/issues/1459.
It would be nice to have helped so that the exception points you in the right direction, for example, the path which was not unsubscribed or the last path segment.
Also, there are alternative methods for doing this listed in this post:
http://brianflove.com/2016/12/11/anguar-2-unsubscribe-observables/
Hope that helps.
In a simple SPA with Vue and Firebase, there are two routes: Login and Chat.
Upon login, the user is redirected to the Chat route where Firebase database bindings are done manually using vuefire's $bindAsArray(), inside the created() life-cycle hook. This is because the bindings require the uid assigned by the Firebase authentication to be available.
This works fine, until the user refreshes the page. If auth().currentUser is used to get the uid, it returns null. If the auth().onAuthStateChanged() watcher is used, Vue attempts to render the component before the Firebase database bindings are done. What am I missing?
I come across this scenario, as workaround I use component wrapper that has UID as property, if UID is null show a waiting message/animation else show your original component.
My real scenario is a little more complex to post it here (firebase, routing, vuex) but basically that wrapper component should look similar to this
<template>
<component :is="CurrentComponent" />
</template>
<script>
import App from './App';
import WaitingAnimation from './WaitingAnimation';
export default {
data() {
return {
Uid: null,
}
},
computed: {
CurrentComponent() {
return this.Uid == null ? WaitingAnimation : App;
}
}
beforeMount() {
//While Firebase is initializing `Firebase.auth().currentUser` will be null
let currentUser = Firebase.auth().currentUser;
//Check currentUser in case you previously initialize Firebase somewhere else
if (currentUser) {
//if currentUser is ready we just finish here
this.Uid = currentUser.uid;
} else {
// if currentUser isn't ready we need to listen for changes
// onAuthStateChanged takes a functions as callback and also return a function
// to stop listening for changes
let authListenerUnsuscribe = Firebase.auth().onAuthStateChanged(user => {
//onAuthStateChanged can pass null when logout
if (user) {
this.Uid = user.uid;
authListenerUnsuscribe(); //Stop listening for changes
}
});
}
}
}
</script>