I use Vue3 Composition API and am trying to explore its reusability possibilities. But I feel that I don't understand how it should be used.
For example, I extracted the login function to a file, to use it on login, and also after registration.
#/services/authorization:
import { useRoute, useRouter } from "vue-router";
import { useStore } from "#/store";
import { notify } from "#/services/notify";
const router = useRouter(); // undefined
const route = useRoute(); // undefined
const store = useStore(); // good, but there is no provide/inject here.
export async function login(credentials: Credentials) {
store
.dispatch("login", credentials)
.then(_result => {
const redirectUrl =
(route.query.redirect as string | undefined) || "users";
router.push(redirectUrl);
})
.catch(error => {
console.error(error);
notify.error(error.response.data.message);
});
}
interface Credentials {
email: string;
password: string;
}
#/views/Login:
import { defineComponent, reactive } from "vue";
import { useI18n } from "#/i18n";
import { login } from "#/services/authorization";
export default defineComponent({
setup() {
const i18n = useI18n();
const credentials = reactive({
email: null,
password: null
});
return { credentials, login, i18n };
}
});
And the problem is that route and router are both undefined, because they use provide/inject, which can be called only during setup() method. I understand why this is happening, but I don't get what is correct way to do this.
Currently, I use this workaround #/services/authorization:
let router;
let route;
export function init() {
if (!router) router = useRouter();
if (!route) route = useRoute();
}
And in Login (and also Register) component's setup() i call init(). But I feel that it's not how it's supposed to work.
Related
I have a simple API route, src/pages/api/me.ts that simply echoes if the user is logged in or not.
import { NextApiRequest, NextApiResponse } from 'next'
import { getSession } from '../../lib/session'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
let user = getSession();
res.status(200).end((user) ? 'yes' : 'no')
}
The import, ../../lib/session (`src/lib/session.ts):
import { cookies } from 'next/headers';
import jwt from 'jsonwebtoken'
export const getSession = () => {
const nxtCookies = cookies();
if (nxtCookies.has('wp_session')) {
const cookie = nxtCookies.get('wp_session');
// prints on the server
console.log('this is happening on the server')
let sessionData = jwt.verify(cookie.value, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return false;
return user;
});
if (sessionData) return sessionData;
}
return false;
}
When I try to call getSession() from pages/api/me.ts, I get an error:
./src/lib/session.ts
You're importing a component that needs next/headers. That only works
in a Server Component but one of its parents is marked with "use
client", so it's a Client Component.
import { cookies } from 'next/headers'; :
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
One of these is marked as a client entry with "use client":
src\lib\session.ts src\pages\api\me.ts
How is this possible? Both are server-sided code.
I even have a server component that uses getSession() to display user information on the website, and this error is not thrown. I even verified via console.log that within both that component and getSession(), that the console prints to the server console. So I am not sure how this is possible.
Specifically, the issue here seems to be the cookie import from next/headers.
I believe cookies from next/headers is only for use in server components. src/pages/api/me.ts
is on the server but is not a server component.
You can access the cookies from a request through req.cookies, and pass it to your getSession function.
A possible implementation of this would be:
// src/pages/api/me.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { getSession } from '../../lib/session'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const user = getSession(req.cookies);
res.status(200).end((user) ? 'yes' : 'no');
}
// src/lib/session.ts
import jwt from 'jsonwebtoken';
export const getSession = (cookies: Partial<Record<string, string>>) => {
const session = cookies.wp_session;
if (session) {
// prints on the server
console.log('this is happening on the server');
const sessionData = jwt.verify(session, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return false;
return user;
});
return sessionData;
}
}
I've been trying to get my Pinia store up and running in Vue 3 and it all has been pretty effortless until I wanted to access some parameters in the url.
I have a store (simplified) like so:
import { defineStore } from 'pinia';
import { useRoute } from 'vue-router';
import { useLocalStorage } from '#vueuse/core';
export const useUserStore = defineStore('user', () => {
const route = useRoute();
const uuid = ref(
useLocalStorage('uuid', route.params.id)
)
return { uuid };
})
Unfortunately, the route remains undefined as if useRoute() is not triggered properly. I've seen that you can add plugins to add the router instance to the pinia store on initialisation, but there's no way I can find to access that this instance in a Setup Store.
Any help would be greatly appreciated
route is not defined when the pinia is initiated.
You need to wait a bit.
One way to do this is to call the function when the component is loaded.
export const useUserStore = defineStore('user', () => {
const route = useRoute();
const id = ref('');
const setId = () => {
id.value = route.params.id as string; // don't need as string if you don't use TypeScript
};
return { id, setId };
});
<script setup lang="ts">
import { useUserStore } from '../stores/user';
const user = useUserStore();
user.setId(); // call the function from pinia, route.params works just fine
</script>
Link Demo
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,
},
}
}
My aim is to validate a users' credentials from vue frontend, fetch their data, store the data in a globally available variable and use them in any component. After some work, I'm able to use Vue's reactive() method with inject and provide. Here's my current code:
In store/index.js
import { reactive, inject } from 'vue'
// global states
export const stateSymbol = Symbol('state')
export const createState = () => reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {},
logIn: userData => {
let user = createState.currentUser
user = userData
console.log(user)
}
})
export const useState = () => inject(stateSymbol)
Then in main.js
import { createApp } from 'vue'
import App from './App.vue'
// store modules
import { stateSymbol, createState } from './store'
// create app
const app = createApp(App)
app.provide(stateSymbol, createState())
Then in login/register component named Login.vue
import { useState } from "../store"
import axios from 'axios'
export default {
name: 'RegisterLogin',
setup() {
let state = useState()
axios.get(getUser, jwtConfig).then(response => {
userId = response.data.id
const currentUser = getUserProfile+userId // URL
console.log(currentUser)
const getCurrentUser = axios.get(currentUser, jwtConfig)
getCurrentUser.then(response => {
state.logIn(response.data)
console.log(state.currentUser)
})
})
}
}
console.log(user) in store/index.js logs the userData as expected. I believe with that, createState.currentUser should be mutated. The problem however is console.log(state.currentUser) logs an empty proxy object. Also, if I try accessing the state.currentUser from another component like Home.vue:
<template>
{{ state.currentUser }}
</template>
<script>
import { useState } from '../store/'
export default {
name: 'Index',
setup() {
return {
state: useState(),
}
},
}
</script>
...an empty object is always displayed even after the user logs in successfully. Is there anything I'm doing wrong? Thank you for your assistance (in advance).
I was looking through the code but couldn't find anywhere where the variable was getting mutated, but I believe I know what's going on.
You are reassigning the user object, not assigning the createState.currentUser to another object.
import { reactive, inject } from 'vue'
// global states
export const stateSymbol = Symbol('state')
export const createState = reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {},
logIn: userData => {
// over here you assign user to createState.currentUser
let user = createState.currentUser;
// then you reassign user to `user data`
user = userData
console.log(user)
}
})
export const useState = () => inject(stateSymbol)
you could instead try
import { reactive, inject } from 'vue'
// global states
export const stateSymbol = Symbol('state')
export const createState = () => reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {},
logIn: userData => {
createState.currentUser = userData
console.log(createState.currentUser)
}
})
export const useState = () => inject(stateSymbol)
As an aside, why are you using inject? I can't figure out what you're expecting it to do.
export const useState = () => stateSymbol would have the same result, but you don't even need to pass the function, you can just use something more concise like...
//////// store
import { reactive } from 'vue'
// global states
export const store = reactive({
backendRoute: 'http://127.0.0.1:8000/',
currentUser: {}
})
export const logIn = (userData) => {
store.currentUser = userData
}
////////////////////////////////////////////////
////// main
import { store, logIn } from "../store"
import axios from 'axios'
export default {
name: 'RegisterLogin',
setup() {
axios.get(getUser, jwtConfig).then(response => {
userId = response.data.id
const currentUser = getUserProfile+userId // URL
console.log(currentUser)
const getCurrentUser = axios.get(currentUser, jwtConfig)
getCurrentUser.then(response => {
logIn(response.data)
console.log(store.currentUser)
})
})
}
}
It appears #Daniel was right after all. My logIn method in store/index.js was not really mutating the currentUser property. In fact, I'm unable to access that property from the method. To solve this, I had to do the mutation immediately after the axios call like state.currentUser = response.data in then method and boom, the state is updated in all components! Thanks.
I'm using a Net Core, React-Redux boiler-plate, and when I run the fetch api action, the reducer state does not change at all.
Here is my action
import axios from "axios";
import config from '../config';
const ROOT_URL = config[process.env.NODE_ENV].api;
export const FETCH_EVENTS = "FETCH_EVENTS";
export function fetchEvents() {
const url = ROOT_URL +"/Event/GetAllEvents";
const request = axios.get(url);
return {
type: FETCH_EVENTS,
payload: request
};
}
my index reducer:
import { combineReducers} from 'redux';
import { routerReducer } from 'react-router-redux';
import dataReducer from './dataReducer'
const reducers = {
events: dataReducer
};
const rootReducer = combineReducers({
...reducers,
routing: routerReducer
});
export default rootReducer;
and my reducer:
import { FETCH_EVENTS } from "../actions/ExtractActions";
export default function (state = [], action) {
switch (action.type) {
case FETCH_EVENTS:
console.log("inside reducer")
return [action.payload, ...state];
}
return state;
}
So I add this code in the Home component:
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchEvents }, dispatch);
}
function mapStateToProps(state) {
return {
events: state.events
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
but when I try to run the action and try to see if the reducer state has changed, I get on console log an empty array for "this.props.events". Even though if I am trying to store api data to the state, I even tried modifying the reducer method and simply returning a string, but this.props.events returns an empty array [] again. I am guessing my redux is not working but I don't know why. I've been debugging all night long
componentWillMount() {
this.props.fetchEvents()
console.log(this.props.events)
}
I found the error. For some reason I had to call this.props.events in the render() method and not componentwillmount.
axios.get() is an async function. That's why you couldn't see the updated state when you logged it right after fetching the events. I would recommend you to use the redux-devtools-extension for debugging. Hope this helps. Cheers!