I'm familiar with Svelte but completely new to Sveltekit. I'm trying to build a Sveltekit app from scratch using AWS Cognito as the authorization tool without using AWS Amplify, using the amazon-cognito-identity-js sdk. I've got all the functionality working as far as login, registration, and verification, but I can't seem to get a handle on how to store the session data for the structure I've built.
I've been trying to translate the strategies from this tutorial, based in React, to Sveltekit -- (AWS Cognito + React JS Tutorial - Sessions and Logging out (2020) [Ep. 3]) https://www.youtube.com/watch?v=R-3uXlTudSQ
and this REPL to understand using context in Svelte ([AD] Combining the Context API with Stores) https://svelte.dev/repl/7df82f6174b8408285a1ea0735cf2ff0
To elaborate, I've got my structure like so (only important parts shown):
src
|
|-- components
|-- ...
|-- status.svelte
|-- routes
|
|-- dashboard
|-- onboarding
|-- __layout.reset.svelte
|-- login.svelte
|-- signup.svelte
|-- verify.svelte
|-- ...
|-- settings
|-- __layout.svelte
|-- index.svelte
|-- styles
|-- utils
|-- cognitoTools.ts
|-- stores.ts
I wanted to have a separate path for my onboarding pages, hence the sub-folder. My cognito-based functions reside within cognitoTools.ts. An example of a few functions look like:
export const Pool = new CognitoUserPool(poolData);
export const User = (Username: string): any => new CognitoUser({ Username, Pool });
export const Login = (username: string, password: string): any => {
return new Promise ((resolve, reject) => User(username).authenticateUser(CognitoAuthDetails(username, password), {
onSuccess: function(result) {
console.log('CogTools login success result: ', result);
resolve(result)
},
onFailure: function(err) {
console.error('CogTools login err: ', err);
reject(err)
}
}))
}
I'm able to then use the methods freely anywhere:
// src/routes/onboarding/login.svelte
import { Login, Pool } from '#utils/cognitoTools'
import { setContext, getContext } from 'svelte'
let username;
let password;
let session = writeable({});
let currentSession;
// Setting our userSession store to variable that will be updated
$: userSession.set(currentSession);
// Attempt to retrieve getSession func defined from wrapper component __layout.svelte
const getSession = getContext('getSession');
const handleSubmit = async (event) => {
event.preventDefault()
Login(username, password, rememberDevice)
.then(() => {
getSession().then((session) => {
// userSession.set(session);
currentSession = session;
setContext('currentSession', userSession);
})
})
}
...
// src/routes/__layout.svelte
...
const getSession = async () => {
return await new Promise((resolve, reject) => {
const user = Pool.getCurrentUser();
if (user) {
user.getSession((err, session) => {
console.log('User get session result: ', (err ? err : session));
err ? reject() : resolve(session);
});
} else {
console.log('get session no user found');
reject();
}
})
}
setContext('getSession', getSession)
Then, I've been trying to retrieve the session in src/components/status.svelte or src/routes/__layout.svelte (as I think I understand context has to be set in the top level components, and can then be used by indirect child components) to check if the context was set correctly.
Something like:
let status = false;
const user = getContext('currentSession');
status = user ? true : false;
I'm running in circles and I know I'm so close to the answer. How do I use reactive context with my current file structure to accomplish this?
I don't know much about the sdk, so I can't help you with your code above. But I also built an app that uses cognito for auth, and I can share some snippets on how to do it from scratch.
Implement a login form. I have my basic app skeleton (navbar, footer, main slot) in _layout.svelte, and it is configured to show the Login.svelte component and not the main slot if the user is not logged in.
file: __layout.svelte
<script context="module">
export async function load({ session }) {
return {
props: {
user: session.user,
}
}
}
</script>
<script>
import "../app.css";
import Login from "$components/Login.svelte";
export let user
</script>
<svelte:head>
<title>title</title>
</svelte:head>
{#if user}
<header>
</header>
<main>
<slot />
</main>
{:else}
<Login />
{/if}
file: Login.svelte
<form action="/" method="GET">
<input type="hidden" name="action" value="signin" />
<button type="submit" >Sign in</button>
</form>
Handle the login
I choose to do this as a svelte endpoint paired with the index. It keeps the routing super simple. You could do separate login.js and logout.js endpoints if you prefer. Just change your url in the form above.
file: index.js
import { v4 as uuid } from '#lukeed/uuid'
import db from '$lib/db'
const domain = import.meta.env.VITE_COGNITO_DOMAIN
const clientId = import.meta.env.VITE_COGNITO_CLIENT_ID
const redirectUri = import.meta.env.VITE_COGNITO_REDIRECT_URI
const logoutUri = import.meta.env.VITE_COGNITO_LOGOUT_URI
export const get = async (event) => {
const action = event.url.searchParams.get('action')
if (action === 'signin') {
// Hard to guess random string. Used to protect against forgery attacks.
// Should add check in callback that the state matches to prevent forgery
const state = uuid()
return {
status: 302,
headers: {
location: `https://${domain}/oauth2/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=openid+email+profile&state=${state}`,
},
}
}
if (action === 'signout') {
// delete this session from database
if (event.locals.session_id) {
await db.sessions.deleteMany({
where: { session_id: event.locals.session_id }
})
}
return {
status: 302,
headers: {
location: `https://${domain}/logout?client_id=${clientId}&logout_uri=${logoutUri}`
}
}
}
return {}
}
Handle the callback from AWS cognito. Again, I have the callback simply point to the root url. All authentication for me is handled at "/". The heavy lifting is done by hooks.js. This is your SK middleware. It for me is the sole arbiter of the user's authentication state, just because I like to keep it easy for me to understand.
file: hooks.js
import { v4 as uuid } from '#lukeed/uuid'
import cookie from 'cookie'
import db from '$lib/db'
const domain = import.meta.env.VITE_COGNITO_DOMAIN
const clientId = import.meta.env.VITE_COGNITO_CLIENT_ID
const clientSecret = import.meta.env.VITE_COGNITO_CLIENT_SECRET
const redirectUri = import.meta.env.VITE_COGNITO_REDIRECT_URI
const tokenUrl = `https://${domain}/oauth2/token`
const profileUrl = `https://${domain}/oauth2/userInfo`
export const handle = async ({ event, resolve }) => {
const cookies = cookie.parse(event.request.headers.get('cookie') || '')
event.locals.session_id = cookies.session_id // this will be overwritten by a new session_id if this is a callback
if (event.locals.session_id) {
// We have a session cookie, check to see if it is valid.
// Do this by checking against your session db or session store or whatever
// If not valid, or if it is expired, set event.locals.session_id to null
// This will cause the cookie to be deleted below
// In this example, we just assume it's valid
}
if ( (!event.locals.session_id) && event.url.searchParams.get('code') && event.url.searchParams.get('state') ) {
// No valid session cookie, check to see if this is a callback
const code = event.url.searchParams.get('code')
const state = event.url.searchParams.get('state')
// Change this to try, catch for error handling
const token = await getToken(code, state)
if (token != null) {
let cognitoUser = await getUser(token)
event.locals.session_id = uuid()
// Add the value to the db
await db.sessions.create({
data: {
session_id: event.locals.session_id,
user_id: cognitoUser.username,
created: Date()
},
})
let user = await db.users.findUnique({
where: {
user_id: cognitoUser.username,
}
})
event.locals.user = user
event.locals.authorized = true
}
}
const response = await resolve(event);
// This will delete the cookie if event.locals.session_id is null
response.headers.set(
'set-cookie',
cookie.serialize('session_id', event.locals.session_id, {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // one week
})
)
return response;
}
export async function getSession(event) {
return {
user: event.locals.user,
}
}
const getToken = async (code, state) => {
let authorization = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
const res = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${authorization}`,
},
body: `grant_type=authorization_code&client_id=${clientId}&code=${code}&state=${state}&redirect_uri=${redirectUri}`
})
if (res.ok) {
const data = await res.json()
return data.access_token
} else {
return null
}
}
const getUser = async (token) => {
const res = await fetch(profileUrl, {
headers: {
Authorization: `Bearer ${token}`,
},
})
if (res.ok) {
return res.json()
} else {
return null
}
}
Lastly, getting the auth state. In a client-side route, this is done via the page load function. I put this in __layout to have it available on all .svelte routes. You can see it at the top of the __layout file above. For SSR endpoints, you can just access event.locals directly.
NOTE: All of the env vars are set in your .env file and will be imported by vite. This only happens when you start your app, so if you add/change them, you need to restart it.
I don't know if this helps at all since it is so different from your app structure, but maybe you will get some ideas from it.
I don't know what the problem is you run into exactly, but one thing that stands out to me is that you are calling setContext when it's "too" late. You can only call getContext/setContext within component initialization. See this answer for more details: Is there a way to use svelte getContext etc. svelte functions in Typescript files?
If this is the culprit and you are looking for a way how to get the session then: Use context in combination with stores:
<!-- setting the session -->
<script>
// ...
const session = writable(null);
setContext('currentSession', session);
// ...
Login...then(() => ...session.set(session));
</script>
<!-- setting the session -->
<script>
// ..
const user = getContext('currentSession');
// ..
status = $user ? true : false;
</script>
Another thing that stands out to me - but is too long/vague for a StackOverflow answer - is that you are not using SvelteKit's features to achieve this behavior. You could look into load and use stuff in __layout to pass the session down to all children. I'm no sure if this is of any advantage for you though since you are maybe planning to do a SPA anyway and therefore don't need such SvelteKit features.
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.
(1/9/2023) Update : SvelteKit now supports server only load functions and Form actions to send requests to the server.
I want to call my database, but I don't want it be able to get accessed by end users by them going to the API endpoint that I set up. I was wondering how I would be able to just call my database from a file in the lib folder and just returning the data there. When I try it I get the error global not defined:
lib/db.js:
import dotenv from "dotenv";
dotenv.config();
import { MongoClient } from "mongodb";
const uri = process.env["MONGODB_URI"];
const options = {
useUnifiedTopology: true,
useNewUrlParser: true,
};
let client;
let clientPromise;
if (!uri) {
throw new Error("Please add your Mongo URI to .env.local");
}
if (process.env["NODE_ENV"] === "development") {
if (!global._mongoClientPromise) {
client = new MongoClient(uri, options);
global._mongoClientPromise = client.connect();
}
clientPromise = global._mongoClientPromise;
} else {
client = new MongoClient(uri, options);
clientPromise = client.connect();
}
export default clientPromise;
routes/items/index.js:
import clientPromise from "$lib/db";
export async function get() {
const client = await clientPromise;
const db = client.db();
const data = await db.collection("items").find({}).toArray();
const items = data.map(({ name }) => ({ name }));
if (items) {
return {
body: {
items,
},
};
}
}
My attempt:
lib/stores/items.js
import clientPromise from "$lib/db";
import { writable } from "svelte/store";
export const items= writable([]);
const fetchItems = async () => {
const client = await clientPromise;
const db = client.db();
const data = await db.collection("items").find({}).toArray();
const items = data.map(({ name }) => ({ name }));
substances.set(items);
};
fetchItems();
Trying the above code in various places always yields a global not defined error in the client.
I found one question from someone with the same problem, but I couldn't figure out how to create a helper file.
Protecting API is done on back-end side. Usually it either server (like NodeJS) or tools Nginx/Apache (proxy, etc.). You're basically looking for Content-Security-Policy topic, which is vaporous but not related to SvelteKit.
Btw, calling DB directly from the Front-end wouldn't be secure and is not possible.
To get data from any database, you should create enpoint
For user authentication, you can create handle hook:
export async function handle({ request, resolve }) {
let user = await authenticate(request)
request.locals.user = user
request.locals.isAuthenticated = !!user
if (request.path.startsWith('/api')) {
if (!user) {
return {
status: 401,
body: JSON.stringify({
error: {
message: 'Unauthorized'
}
})
}
}
const response = await resolve(request)
return response
}
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!
the problem
I created a Shopify node.js app using the Shopify CLI and I want to display a simple bar under the header using a script tag. I used the script tag API to add a script tag
"script_tags": [
{
"id": 174240039086,
"src": "https://xxxxx.ngrok.io/script_tag",
}
]
And I also added a <div id="script-app"></div> into the theme, under the header.
Here is my script_tag.js file, located in /pages/script_tag.js
import ReactDOM from 'react-dom';
class TestScriptTag extends React.Component {
constructor() {
super();
}
render() {
return (
<div>
<p>this is a bar</p>
</div>
);
}
}
ReactDOM.render(<TestScriptTag />, document.getElementById('script-app'));
export default TestScriptTag;
Lastly, here is my server.js (most of it is what came with the CLI):
import "#babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "#shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "#shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
import { flushSync } from "react-dom";
const fs = require('fs');
dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8083;
const dev = process.env.NODE_ENV !== "production";
const app = next({
dev,
});
const handle = app.getRequestHandler();
Shopify.Context.initialize({
API_KEY: process.env.SHOPIFY_API_KEY,
API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
SCOPES: process.env.SCOPES.split(","),
HOST_NAME: process.env.HOST.replace(/https:\/\//, ""),
API_VERSION: ApiVersion.October20,
IS_EMBEDDED_APP: false,
// This should be replaced with your preferred storage strategy
SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});
// Storing the currently active shops in memory will force them to re-login when your server restarts. You should
// persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};
app.prepare().then(async () => {
const server = new Koa();
const router = new Router();
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
async afterAuth(ctx) {
console.log("here")
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
const host = ctx.query.host;
ACTIVE_SHOPIFY_SHOPS[shop] = scope;
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: "/webhooks",
topic: "APP_UNINSTALLED",
webhookHandler: async (topic, shop, body) =>
delete ACTIVE_SHOPIFY_SHOPS[shop],
});
if (!response.success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
ctx.redirect(`/?shop=${shop}&host=${host}`);
},
})
);
const handleRequest = async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
};
router.get("/", async (ctx) => {
const shop = ctx.query.shop;
// This shop hasn't been seen yet, go through OAuth to create a session
if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
ctx.redirect(`/auth?shop=${shop}`);
} else {
await handleRequest(ctx);
}
});
router.get("/script_tag", (ctx) => {
handleRequest(ctx);
});
router.get("(/_next/static/.*)", handleRequest); // Static content is clear
router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
router.get("(.*)", verifyRequest(), handleRequest); // Everything else must have sessions
server.use(router.allowedMethods());
server.use(router.routes());
server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
I am getting the error: document not defined.
What I've tried
I thought this is due to server side rendering, so I thought I could get around it by doing this:
if (typeof window !== "undefined") {
ReactDOM.render(<TestScriptTag />, document.getElementById('script-app'));
}
But still nothing renders and I get this when I inspect the shop page.
I've also tried changing the routing to this:
router.get("/script_tag", (ctx) => {
ctx.type = "module";
ctx.body = fs.createReadStream('./pages/script_tag.js')
});
But then I get an error about the import statement in script_tag.js - SyntaxError: Unexpected identifier '{'. import call expects exactly one argument.
I'm not sure what the proper way is to serve the javascript file I want to inject into the header. I feel like I'm missing something stupid. Please help!!
From nuxt auth website I saw this:
setUserToken(token)
Returns: Promise
Set the auth token and fetch the user using the new token and current strategy.
TIP: This function can properly set the user after registration
this.$auth.setUserToken(token)
.then(() => this.$toast.success('User set!'))
Tried to use it and it said method is undefined, looked up in the source files and none of methods are like this one.
I am not very good with this but, how would I set user and token with nuxt/auth module after registration or anything but login/loginWith?
If there is no option for that why is it there on documentation?
I would also need to know if I need to create custom auth do I need to use both cookies and localstorage or just one of them?
It says that cookies are used for server side and storage for client side.
Can I use just cookies and on nuxtServerInit get cookie for token and set token and user data fetched by api within vuex store? Then use it from there if it is needed?
Nuxt/auth module hurt my brain so long and today I created custom module:
First I have this store structure:
store/
-- index.js
-- mutations.js
-- actions.js
-- state.js
-- getters.js
middleware/
-- redirectIfAuth.js
-- redirectIfNotAuth.js
layouts/
default.vue -> has redirectIfNotAuth.js
guest.vue -> has redirectIfAuth.js
pages/
-- login/
---- index.vue -> uses guest.vue as layout
-- dashboard/
----- index.vue -> uses default.vue as layout without declaration
Inside Index.js I have:
import state from './state'
import * as actions from './actions'
import * as mutations from './mutations'
import * as getters from './getters'
export default {
state,
getters,
mutations,
actions,
modules: {}
}
Inside State.js I have:
export default () => ({
user: null,
token: null,
headers: null
})
Inside Actions.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
// importing server based cookie library
export async function nuxtServerInit ({ commit }, { req, res }) {
// If we have any axios requests we need to add async/await
// And since this works on server mode, we don't need to check is it server
let token = null
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
// If we have token within cookies we get user data from api and we pass Autorization headers with token
if (token !== null && token !== false) {
await axios.get('/api/auth/me', {
headers: {
'Authorization': token
}
}).then((response) => {
// If we get user data we set it to store
commit('setUser', response.data.data)
commit('setToken', token)
commit('setHeaders', token)
}).catch((error) => {
// If we get error, we should logout user by removing data within cookies and store
// Additionally you can create specific code error on backend to check if token is expired or invalid
// and then check for status code and then remove data
commit('setUser', null)
commit('setToken', null)
res.setHeader('Set-Cookie', [`authToken=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`])
// This is only way I found useful for removing cookies from node server
console.warn(error)
})
}
}
Inside Mutations.js I have:
export const setToken = (state, payload) => state.token = payload
export const setUser = (state, payload) => state.user = payload
export const setHeaders = (state, payload) => {
state.headers = {
headers: {
'Authorization': payload
}
}
}
Inside Getters.js I have:
export const getUser = (state) => state.user
export const getToken = (state) => state.token
export const getHeaders = (state) => state.headers
Second I created two middlewares and it seems like nuxt middlewares work on both server and client sides, so I needed to require both libraries for server and client side Then I checked which side it is and then try to get token for further investigations If you include and don't check for server and client but use one of them, your templates wont render but show undefined errors for req on client instead and on server it wont show anything.
Inside redirectIfAuth.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
const Cookie = process.client ? require('js-cookie') : undefined
export default function ({ app, redirect, req }) {
let token = null
if (process.server) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
} else if (process.client) {
token = Cookie.get('authToken')
}
if (token && token !== false) {
app.store.commit('setToken', token)
app.store.commit('setHeaders', token)
if (app.store.state.user) {
if (app.store.state.user.roles.includes('customer')) {
return redirect({
name: 'customer-slug',
params: { slug: app.store.state.user.username }
})
} else if (app.store.state.user.roles.includes('admin')) {
return redirect({
name: 'dashboard'
})
} else {
return redirect({
name: 'index'
})
}
} else {
return redirect({
name: 'index'
})
}
}
}
Inside redirectIfNotAuth.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
const Cookie = process.client ? require('js-cookie') : undefined
export default function ({ app, redirect, route, req }) {
let token = null
if (process.server) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
} else if (process.client) {
token = Cookie.get('authToken')
}
if (token === null || token === false) {
return redirect({
name: 'login',
query: {
redirect: route.fullPath
}
})
}
}
Now we use these middlewares within pages or layouts as:
export default {
middleware: ['redirectIfAuth']
}
Or
export default {
middleware: ['redirectIfNotAuth']
}
Login:
async login () {
if (this.form.email !== '' && this.form.password !== '') {
await this.$axios.post('/api/auth/login', this.form).then((response) => {
this.$store.commit('setUser', response.data.data)
this.$store.commit('setToken', 'Bearer ' + response.data.meta.access_token)
this.$store.commit('setHeaders', 'Bearer ' + response.data.meta.access_token)
Cookie.set('authToken', 'Bearer ' + response.data.meta.access_token, { expires: 365 })
// Cookie.set('authUser', response.data.data, { expires: 365 }) if you need user data within cookies
if (this.$route.query.redirect) {
this.$router.push(this.$route.query.redirect)
}
this.$router.push('/')
})
}
}
Logout:
async logout () {
await this.$axios.post('/api/auth/logout', {}, this.headers)
// Cookie.remove('authUser') if exists
Cookie.remove('authToken')
this.$router.push('/')
}
I hope this helps someone or someone get idea from this to make something else. I had million problems with official nuxt auth and only this helped me sort things out...