Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
147114 stories
·
33 followers

Ember 6.10 Released

1 Share

The Ember project is excited to announce the release of Ember v6.10. This is a standard minor release as part of the standard Ember Release Train process. This release takes a big swing at cleaning up the blueprint for newly generated Ember apps by updating deprecated dependencies and upgrading the use of WarpDrive and Glint 🎉 Keep reading to find out more!

Ember.js 6.10

Ember.js 6.10 does not introduce any new features, but we have added one deprecation related to how the ember-source package will be published going forward.

Deprecating Ember Vendor Bundles

Today, the published ember-source package contains several AMD-specific bundled builds of Ember that are appended to vendor.js in the classic build system. This used to be the main way that people got the Ember framework code into their apps before we started generating new apps with Vite and Embroider in Ember 6.8. Since then, for newly generated apps, the Vite build system would access the ESM sources in the published ember-source package directly.

For anyone who has not yet upgraded their build system to Vite, they will still be getting the old build behaviour where the pre-built AMD bundles will be added to vendor.js. The newly added deprecation will mean that after Ember 7.0 we will no longer publish ember-source with these pre-built AMD bundles and apps that are on the classic build system will start to consume ember-source via ember-auto-import in the same way as any other v2 addon (ember-source has been published as a v2 addon since Ember 6.1). Most apps won't notice any difference once this change happens, and if you want to opt-into the behaviour early (and silence the deprecation) you can enable the new use-ember-modules optional feature which is described in the AMD bundles deprecation guide.

For more information and motivation you can check out the Deprecate Ember Vendor Bundles RFC

Ember CLI v6.10

Ember CLI has made a significant improvement to the number of reported package deprecations when installing a new application. There have also been some modernisations added to the default @ember/app-blueprint so that new apps are genrated using the @warp-drive/* packages and default WarpDrive setup (instead of the old ember-data pacakge), and Typescript apps are now generated with Glint 2. We have also stopped installing tracked-built-ins in newly generated ember apps because they are actually built-in now, and we have finally dropped ember-auto-import from the default blueprint.

Note: ember-cli has updated its required Node version to at least v20.19.0 because some of the dependency updates need support for requiring ESM

Package updates

Usually updating dependencies doesn't make it to the highlight reel of a release, but this has been a major effort by Bert De Block who painstakingly went through all the dependencies of ember-cli and updated them all to the latest major and made any necessary changes as part of that upgrade. This ember-cli version also brings in the new major version of broccoli which itself has had a bit of a package cleanup thanks to Katie Gengler. There is an ongoing effort to clean up all the remaining npm package deprecations when generating a new Ember app so watch this space for more updates!

Modern WarpDrive Packages

In case you didn't know, ember-data is in the process of rebranding itself to WarpDrive. It's mosly the same ember-data that we know and love, but it's starting to feel a lot more modern and fits in a lot better with the increasingly modern-feeling Ember ecosystem. For the past few versions ember-data has already been powered by the new @warp-drive/* packages, but this version is the first time that we drop the old ember-data package and start using WarpDrive packages directly.

Everything you are used to in ember-data should still work because for now the WarpDrive setup is being installed with legacyMode turned on. We haven't yet updated the Ember Guides to start using the more modern WarpDrive features (like schemas) so that will be something we communicate more about in upcoming releases

Glint v2

Glint is the system that Ember uses to allow type-checking of your Glimmer templates. When Glint was first created it did a number of weird and wonderful hacks on top of tsc to get things to work, but this was hard to manage and always a bit brittle. Glint v2 is built on top of the Volar.js Embedded Language Tooling Framework and is specifically designed to work very well with GTS files. In this release we are generating new apps that use the --typescript flag with Glint v2 set up. If you are upgrading you can check out the Glint v2 upgrade guide for more information.

tracked-built-ins actually built-in

tracked-built-ins is a very useful addon with a collection of utilities like TrackedObject, TrackedArray, TrackedMap, etc. that we have been installing by default in new Ember apps since Ember v5.2. These utilities have been available in ember-source directly since v6.8 so in this release we have removed tracked-built-ins from the default addons in new applications.

ember-auto-import removed

Since we moved to using the Vite build system by default in v6.8 ember-auto-import has been completely inactive in new applications, but we weren't able to remove it from dependencies because of a bug. This may seem like a small thing to highlight in the release, but it has a decently large impact because even though we weren't actively using webpack for anything in the new build system, ember-auto-import was pulling in webpack as a peer dependency. So this change will stop you either installing unnecessary packages, remove missing peer warnings, or stop it being an error (depending on your package manager's approach to peers).

Thank You!

As a community-driven open-source project with an ambitious scope, each of these releases serves as a reminder that the Ember project would not have been possible without your continued support. We are extremely grateful to our contributors for their efforts.

Read the whole story
alvinashcraft
5 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

PPP 495 | Your Layoff Survival Playbook: Lessons from The Layoff Journey, with Steve Jaffe

1 Share

Summary

In this episode, Andy talks with Steve Jaffe, author of The Layoff Journey: From Dismissal to Discovery. Steve has been laid off four times over the course of his career, and those experiences shaped a thoughtful, practical framework for navigating the emotional and professional aftermath of job loss.

Andy and Steve explore why layoffs feel so personal even when we are told they are not, how identity often gets tangled up with job titles, and why the emotional response to a layoff closely mirrors the stages of grief. Steve explains why those stages are not linear, what denial, pain, and negotiation really look like in practice, and why trying to rush straight to acceptance can backfire.

You will also hear practical advice for leaders who must conduct layoffs, as well as guidance for professionals who worry they might be laid off in the future. From preserving dignity in difficult conversations to preparing financially, emotionally, and professionally before uncertainty hits, this discussion offers insight for both sides of the table.

If you are navigating uncertainty, supporting others through change, or simply want to be better prepared for whatever comes next, this episode is for you!

Sound Bites

  • "I wanted to give people a roadmap to process their layoff and the grief of their layoff in months rather than years."
  • "One of the things that makes losing a job difficult is we tie our identity up in what we do."
  • "And then in that period, before you've landed your next job, you're in this messy middle of Who am I?"
  • "Define yourself not by what you do, but by who you are and what you bring to the table."
  • "I've seen people be named Employee of the Year in January, and by June they're getting laid off."
  • "Layoffs don't measure your worth. They measure a company's priorities."
  • "The stages of grief are not linear. You can feel all of them in one day."
  • "Your job title is not who you are."
  • "Acceptance can become a way to skip discomfort instead of dealing with loss."
  • "If you don't process the grief, it shows up later as baggage."
  • "Dignity matters in the first minutes of a layoff conversation."
  • "You want to build your network before you need it."
  • "The person you were before a layoff will not be the same person after."

Chapters

  • 00:00 Introduction
  • 01:45 Start of Interview
  • 02:00 From First Layoff to Fourth: Taking It Personally
  • 02:50 How the Layoff Process Has Changed Over Time
  • 06:52 The Messy Middle Between Job Loss and What's Next
  • 10:40 Why the Stages of Grief Apply to Layoffs
  • 14:07 What Denial Looked Like in Steve's Experience
  • 17:19 Balancing Emotional Honesty and Professional Reputation
  • 22:08 The Quote That Opens the Book
  • 23:00 Can You Jump to Acceptance Too Quickly?
  • 24:58 When Past Layoffs Create Baggage at the Next Job
  • 26:42 Advice for Leaders Who Have to Do Layoffs
  • 28:55 Handling Performance-Based Separations with Integrity
  • 30:40 How to Prepare Now If You Worry About Being Laid Off
  • 32:46 End of Interview
  • 33:33 Andy Comments After the Interview
  • 37:37 Outtakes

Learn More

You can learn more about Steve and his work at TheSteveJaffe.com.

For more learning on this topic, check out:

  • Episode 163. A short three-minute video Andy put together about what to do before losing your job.
  • Episode 310 with Jeff Gothelf, about how to let your next job find you.
  • Episode 230 with Scott Belsky. Not specifically about layoffs, but full of insights on careers, growth, and the hiring process.

Level Up Your AI Skills

In the outtakes, Andy and Steve talk about how AI is changing the workplace. If you want to be better prepared for an AI-infused future, check out our AI Made Simple course.

Just go to ai.PeopleAndProjectsPodcast.com. Thanks!

Pass the PMP Exam

If you or someone you know is thinking about getting PMP certified, we've put together a helpful guide called The 5 Best Resources to Help You Pass the PMP Exam on Your First Try. We've helped thousands of people earn their certification, and we'd love to help you too.

Just go to 5BestResources.PeopleAndProjectsPodcast.com to grab your copy. I'd love to help you get your PMP this year!

Join Us for LEAD52

I know you want to be a more confident leader. That's why you listen to this podcast. LEAD52 is a global community of people like you who are committed to transforming their ability to lead and deliver. It's 52 weeks of leadership learning, delivered right to your inbox, taking less than five minutes a week. And it's all for free.

Learn more and sign up at GetLEAD52.com. Thanks!

Thank you for joining me for this episode of The People and Projects Podcast!

Talent Triangle: Business Acumen

Topics: Leadership, Layoffs, Career Transitions, Organizational Change, Emotional Intelligence, Resilience, Identity at Work, Grief, Workforce Planning, Change Management, Professional Development

The following music was used for this episode:

Music: Echo by Alexander Nakarada
License (CC BY 4.0): https://filmmusic.io/standard-license

Music: Energetic Drive Indie Rock by WinnieTheMoog
License (CC BY 4.0): https://filmmusic.io/standard-license





Download audio: https://traffic.libsyn.com/secure/peopleandprojectspodcast/495-SteveJaffe.mp3?dest-id=107017
Read the whole story
alvinashcraft
5 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

AspiriFridays - AI or something

1 Share
From: aspiredotdev
Duration: 0:00
Views: 43

Maddy, Fowler, Damian, and the rest of the Aspire team have been busy building Aspire, but we wanted to get some AspiriFridays time in just because. Who knows what today will bring?!

Read the whole story
alvinashcraft
5 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Compliance Meets AI: What’s New in Insider Risk Management for Copilot

1 Share

AI is moving fast—and so are the risks. That’s exactly why our latest Compliance Meets AI session, “What’s New – Insider Risk Management for Copilot,” focused on how organizations can confidently secure data in an increasingly agent-driven world.

In this session, Kevin Uy walked through what’s new (and what’s next) in Microsoft Purview Insider Risk Management, with a strong spotlight on Copilot, AI agents, and Security Copilot integration. From real-world risk scenarios to live demos, this was a practical deep dive into protecting your organization while still empowering innovation.

If you missed the session we have you covered!  You can find the recording below.

https://aka.ms/Compliance-Meets-Ai-Insider-Risk-Management 

🔍 Key highlights included:

  • Insider Risk for Copilot & Agents – How Microsoft now monitors both humans and AI agents with purpose-built policies.
  • Risky Agents (Preview) – New capabilities that provide visibility and governance for agents built in Copilot Studio and Azure AI Foundry.
  • Security Copilot Triage Agents – A powerful look at AI helping security teams prioritize what truly matters by summarizing and contextualizing Insider Risk and DLP alerts.
  • DSPM for AI – How organizations can gain insight into risky AI usage, prompts, and responses—inside and outside the Microsoft ecosystem.

đź’ˇ One of the biggest takeaways? AI security no longer stops at users. Agents operate at machine speed, and organizations need the same level of governance, risk scoring, and investigation capabilities to keep pace.

If you’re responsible for security, compliance, data protection, or AI governance, this session is packed with insights you can apply right away.

All past recordings Can be found here Jay Cotton - YouTube and dont forget to register for our session over the next three weeks here

Compliance Meets Ai 2026: Microsoft Purview in the Age of Ai | Microsoft Community Hub 

 

Read the whole story
alvinashcraft
5 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Updated App Review Guidelines now available

1 Share

The App Review Guidelines have been revised to clarify that apps with random or anonymous chat are subject to the 1.2 User-Generated Content guideline.

Translations of the guidelines will be available on Apple Developer website within one month.

Read the whole story
alvinashcraft
6 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Pure Surreal Database Authentication in Nuxt

1 Share

See how Nuxt pairs up with Surreal Database with a sample app enabling login, register, logout and password change options with route guards and server-safe APIs.

I am obsessed with Surreal Database. I love graph databases, and this database allows us to avoid joins forever!

Dashboard with user ID, change password, home, logout, dashboard

TL;DR

This app allows you to connect to Surreal Database and use the first class login system with Nuxt 4. You can login, register, logout and change your password with route guards and server-safe APIs.

Surreal Database Setup

First, set up a cloud instance with Surreal Cloud. This example should also work with a local version.

I’m using Surreal 2.3.10 or later.

Schema

We must declare our Surreal Schema with sign in and sign up functions.

--  ACCESS RULE
DEFINE ACCESS user
    ON DATABASE
    TYPE RECORD
    SIGNUP (
        CREATE users SET
            username = $username,
            password = crypto::argon2::generate($password)
    )
    SIGNIN (
        SELECT * FROM users
        WHERE username = $username
          AND crypto::argon2::compare(password, $password)
    )
    WITH JWT
        ALGORITHM HS512
        KEY 'YOUR_SUPER_SECRET_KEY_HERE'
        DURATION
            FOR TOKEN 1h,
            FOR SESSION NONE;

--  USERS TABLE
DEFINE TABLE users
    TYPE NORMAL
    SCHEMAFULL
    PERMISSIONS
        FOR select, update WHERE id = $auth.id,
        FOR create, delete NONE;

--  FIELD DEFINITIONS
DEFINE FIELD username
    ON users
    TYPE string
    ASSERT $value != ''
    PERMISSIONS FULL;

DEFINE FIELD password
    ON users
    TYPE string
    ASSERT $value != ''
    PERMISSIONS FULL;

--  UNIQUE INDEX
DEFINE INDEX usernameIndex
    ON TABLE users
    COLUMNS username
    UNIQUE;
  • SIGNIN and SIGNUP functions must be declared and will store the username and encrypted password in the database.
  • We use users, but user could work as well, depending on your preference.
  • Currently, for proper permissions, you need a SCHEMAFULL table declared. I also added a usernameIndex, which also enables uniqueness.

Currently Surreal ONLY supports the before state in an update statement, so this could be a HUGE security risk. I have created an issue for it. You could work around this with a trigger function.

Surreal JS

Install the latest version. We are still in alpha here.

npm i surrealdb@alpha

Basic Nuxt Setup

I just created a blank Nuxt app and installed Tailwind.

npm create nuxt@latest

I didn’t use the package, but vanilla Tailwind installation.

Environment Variables

Put your database info in the .env file for easy access. You don’t want to push this up to GitHub, although it wouldn’t be the worst thing.

NUXT_SURREAL_URL=...
NUXT_SURREAL_NAMESPACE=...
NUXT_SURREAL_DATABASE=...

The NUXT_ prefix allows us to use the variables anywhere on the server.

nuxt.config.ts

We can add placeholders here that will automatically import the variables.

import tailwindcss from "@tailwindcss/vite"

// <https://nuxt.com/docs/api/configuration/nuxt-config>
export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  devtools: { enabled: true },
  css: ['./app/assets/css/main.css'],
  vite: {
    plugins: [
      tailwindcss()
    ]
  },
  runtimeConfig: {
    surrealDatabase: '',
    surrealNamespace: '',
    surrealUrl: ''
  }
})

Surreal Functions

We must create a server instance.

Create a Server

export async function createSurrealServer(event: H3Event) {

    const config = useRuntimeConfig()

    const db = new Surreal()

    try {
        await db.connect(config.surrealUrl, {
            namespace: config.surrealNamespace,
            database: config.surrealDatabase
        })
    } catch (error) {
        if (error instanceof Error) {
            console.error(error)
            return {
                error,
                data: null
            }
        }
        return {
            error: new Error('Unknown connection error'),
            data: null
        }
    }

    const surrealToken = getCookie(
        event,
        SURREAL_COOKIE_NAME
    )

    if (surrealToken) {
        await db.authenticate(surrealToken)
    }

    return {
        data: db,
        error: null
    }
}
  • We get the useRuntimeConfig() for our database variables to connect.
  • Unfortunately, we have to use try / catch for error handling, but I’m hoping to get this fixed with Supabase-like error destructuring.
  • We create an class instance with Surreal().
  • We connect with db.connect(). However, if we’re using http version, this just setups up our REST call.
  • Surreal is 100% compatible outside of Node.js with an HTTP version. We just don’t use wss:// and replace it with http:// version of our URL. This is huge for Edge Computing, Bun, Deno, Cloudflare and any other V8 Isolates.
  • We check for a token, and if there is one, add it to the next authentication header with db.authenticate().

Login and Register

The login and register functions are nearly identical.

export async function surrealLogin(event: H3Event, username: string, password: string) {

    const config = useRuntimeConfig()

    const { data: db, error: dbError } = await createSurrealServer(event)

    if (dbError) {
        if (dbError instanceof Error) {
            console.error(dbError)
            return {
                error: dbError,
                data: null
            }
        }
        return {
            error: new Error('Unknown login error'),
            data: null
        }
    }

    if (!db) {
        return {
            data: null,
            error: new Error("No SurrealDB instance")
        }
    }

    try {
        const auth = await db.signin({
            namespace: config.namespace,
            database: config.database,
            variables: {
                username,
                password
            },
            access: 'user'
        })

        const { token } = auth

        setCookie(
            event,
            SURREAL_COOKIE_NAME,
            token,
            COOKIE_OPTIONS
        )

        return {
            data: token,
            error: null
        }

    } catch (signInError) {

        surrealLogout(event)

        if (signInError instanceof Error) {
            console.error(signInError)
            return {
                error: signInError,
                data: null
            }
        }
        return {
            error: new Error('Unknown sign-in error'),
            data: null
        }
    }
}

export async function surrealRegister(event: H3Event, username: string, password: string) {

    const config = useRuntimeConfig()

    const { data: db, error: dbError } = await createSurrealServer(event)

    if (dbError) {
        return {
            data: null,
            error: dbError
        }
    }

    if (!db) {
        return {
            data: null,
            error: new Error("No SurrealDB instance")
        }
    }

    try {
        const auth = await db.signup({
            namespace: config.namespace,
            database: config.database,
            variables: {
                username,
                password
            },
            access: 'user'
        })

        const { token } = auth

        setCookie(
            event,
            SURREAL_COOKIE_NAME,
            token,
            COOKIE_OPTIONS
        )

        return {
            data: token,
            error: null
        }

    } catch (signUpError) {

        surrealLogout(event)

        if (signUpError instanceof Error) {
            console.error(signUpError)
            return {
                error: signUpError,
                data: null
            }
        }
        return {
            error: new Error('Unknown sign-up error'),
            data: null
        }
    }
}
  • We get our server instance with our createSurrealServer() function.
  • We configure our login:
{
    namespace: config.namespace,
    database: config.database,
    variables: {
        username,
        password
    },
    access: 'user'
}

This must match our configuration.

  • We set the cookie and return the token.
  • Same code for signin and signup.

Logout

We just delete the cookie.

export function surrealLogout(event: H3Event) {

    deleteCookie(
        event,
        SURREAL_COOKIE_NAME,
        COOKIE_OPTIONS
    )
}

Change Password

We need to be able to change our password.

export async function surrealChangePassword(
    event: H3Event,
    currentPassword: string,
    newPassword: string
) {

    const { data: db, error: dbError } = await createSurrealServer(event)

    if (dbError) {
        return {
            data: null,
            error: dbError
        }
    }

    if (!db) {
        return {
            data: null,
            error: new Error("No SurrealDB instance")
        }
    }

    try {

        const { data: userId } = await getCurrentUserId(event)

        if (!userId) {
            return {
                data: null,
                error: null
            }
        }

        const query = `
            UPDATE $id
            SET password = crypto::argon2::generate($new)
            WHERE crypto::argon2::compare(password, $old)
        `

        const [result] = await db.query(query, {
            id: new RecordId('users', userId),
            old: currentPassword,
            new: newPassword
        }).collect<[{ id: string, password: string, username: string }][]>()

        if (!result) {
            return {
                data: null,
                error: new Error("Password change failed")
            }
        }
        return {
            data: result[0],
            error: null
        }
    } catch (error) {
        if (error instanceof Error) {
            console.error(error)
            return {
                error,
                data: null
            }
        }
        return {
            error: new Error('Unknown query error'),
            data: null
        }
    }
}
  • Our query updates the password where the current password and current user ID are equal.
  • We use db.query() to query the database and safely pass in our id, old password and new password.
  • The collect() method helps us get the typing right. I will return an array with an array.
  • We use a RecordId instance to handle our users:0001 id query.

There will be a db.select() function in the future, but it will not handle advanced queries yet, so I didn’t include it.

Get Current User ID

We need to get the user ID from our cookie token, or from the database itself.

export async function getCurrentUserId(event: H3Event, refetch = false) {

    const token = getCookie(event, SURREAL_COOKIE_NAME)

    if (!token) {
        return {
            data: null,
            error: null
        }
    }

    if (refetch) {

        const {
            data: db,
            error: dbError
        } = await createSurrealServer(event)

        if (dbError) {
            return {
                data: null,
                error: dbError
            }
        }

        if (!db) {
            return {
                data: null,
                error: new Error("No SurrealDB instance")
            }
        }

        try {
            const userId = (await db.auth())?.id.id.toString()

            if (!userId) {
                return {
                    data: null,
                    error: null
                }
            }
            return {
                data: userId,
                error: null
            }

        } catch (error) {
            if (error instanceof Error) {
                console.error(error)
                return {
                    error,
                    data: null
                }
            }
            return {
                error: new Error('Unknown authentication error'),
                data: null
            }
        }
    }

    const userId = parseToken(token)

    return {
        data: userId,
        error: null
    }
}
  • We can fetch the logged-in user directly from the database with db.auth() for the safest route. This is imperative for critical operations like password changes, deleting and certain updates.

However, to save us an extraneous database call for most non-critical operations, the cookie will suffice.

export function parseToken(token: string) {
    return JSON.parse(atob(token.split('.')[1])).ID.split(':')[1] as string
}

Nuxt Server APIs

Next, we must create our Nuxt Endpoints to handle the Surreal data.

Login / Redirect

// server/api/login.post.ts

import { parseToken, surrealLogin } from "../utils/surreal"

export default defineEventHandler(async (event) => {

    const body = await readBody<{
        username: string
        password: string
    }>(event)

    const { username, password } = body

    if (!username || !password) {
        throw createError({
            statusCode: 400,
            message: 'Missing credentials'
        })
    }

    const {
        data: token,
        error: loginError
    } = await surrealLogin(event, username, password)

    if (loginError) {
        throw createError({
            statusCode: 500,
            message: 'Login failed',
            data: loginError.message
        })
    }

    if (!token) {
        throw createError({
            statusCode: 401,
            message: 'Invalid credentials',
            data: token
        })
    }

    const userId = parseToken(token)

    if (!userId) {
        throw createError({
            statusCode: 401,
            message: 'Unauthorized',
            data: userId
        })
    }

    sendRedirect(event, '/')
})

// server/api/register.post.ts

import { parseToken, surrealRegister } from "../utils/surreal"

export default defineEventHandler(async (event) => {

    const body = await readBody<{
        username: string
        password: string
    }>(event)

    const { username, password } = body

    if (!username || !password) {
        throw createError({
            statusCode: 400,
            message: 'Missing credentials'
        })
    }

    const {
        data: token,
        error: registerError
    } = await surrealRegister(event, username, password)

    if (registerError) {
        throw createError({
            statusCode: 500,
            message: 'Registration failed',
            data: registerError.message
        })
    }

    if (!token) {
        throw createError({
            statusCode: 401,
            message: 'Invalid credentials',
            data: token
        })
    }

    const userId = parseToken(token)

    if (!userId) {
        throw createError({
            statusCode: 401,
            message: 'Unauthorized',
            data: userId
        })
    }

    sendRedirect(event, '/')
})
  • We get our data from readBody and error out with createError.
  • A successful login redirects home with sendRedirect.
  • We add .post.ts to the end so that it gets called correctly.
  • Again, both register and login are very similar.

Logout

Nothing to this but calling our logout function and redirecting.

// server/api/logout.get.ts

import { surrealLogout } from "../utils/surreal"

export default defineEventHandler((event) => {
    surrealLogout(event)
    sendRedirect(event, "/")
})

Password

You can start to see the pattern of calling our functions.

// server/api/password.post.ts

import { surrealChangePassword } from "../utils/surreal"

export default defineEventHandler(async (event) => {

    const body = await readBody<{
        current_password: string
        new_password: string
    }>(event)

    const { current_password, new_password } = body

    if (!current_password || !new_password) {
        throw createError({
            statusCode: 400,
            message: 'Missing credentials'
        })
    }

    const {
        data: newRecord,
        error: changePasswordError
    } = await surrealChangePassword(
        event,
        current_password,
        new_password
    )

    if (changePasswordError) {
        throw createError({
            statusCode: 500,
            message: 'Change password failed',
            data: changePasswordError.message
        })
    }

    if (!newRecord?.id) {
        throw createError({
            statusCode: 401,
            message: 'Invalid credentials',
            data: newRecord
        })
    }

    sendRedirect(event, '/')
})
  • Same deal. Run function, redirect.

Get User

We must get the user, or user ID, from our function to have it available on the client.

// server/api/user.get.ts

import { getCurrentUserId } from "../utils/surreal"

export default defineEventHandler(async (event) => {
    const { data: userId } = await getCurrentUserId(event)
    return { userId }
})

Middleware and Composables

We must protect our routes and get our data.

useAuth

This runs on the server and client, but fetches from the server.

// composables/useAuth.ts

export async function useAuth() {

    const { data, error } = await useFetch<{ userId: string | null }>("/api/user", {
        server: true,
        default: () => ({ userId: null }),
        lazy: false
    })

    if (error.value) {
        console.error(error.value)
    }

    const userId = computed(() => data.value?.userId || null)

    return { userId }
}
  • Run on server
  • Block navigation until complete
  • Default to null
  • Get userId signal

Guards

We must protect our routes before loaded on the client.

// middleware/authGuard.ts

export default defineNuxtRouteMiddleware(async () => {
  const { userId } = await useAuth()
  if (!userId.value) {
    return navigateTo('/login', { redirectCode: 302 })
  }
})
  • Must be logged in to view page.
export default defineNuxtRouteMiddleware(async () => {
  const { userId } = await useAuth()
  if (userId.value) {
    return navigateTo('/dashboard', { redirectCode: 302 })
  }
})
  • Should never see login or register if you’re signed in.

Pages in App

In Nuxt 4, we can put everything in the app folder.

Layout

We create our shared layout in app.vue.

<script setup lang="ts">
const { userId } = await useAuth()
</script>

<template>
  <main class="mt-10 flex flex-col items-center justify-center space-y-6">
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
    <nav class="flex space-x-6">
      <NuxtLink to="/">Home</NuxtLink>
      <template v-if="userId">
        <a href="https://www.telerik.com/api/logout"> Logout </a>
      </template>
      <template v-else>
        <NuxtLink to="/login">Login</NuxtLink>
      </template>
      <NuxtLink to="/dashboard">Dashboard</NuxtLink>
    </nav>
  </main>
</template>
  • NuxtPage is our slot.
  • NuxtLink makes sure we use our router.
  • NuxtLayout decides where it goes.

Register and Login

Our register and login pages just display a form and our route guard.

// app/pages/register.vue

<script setup lang="ts">
definePageMeta({
  middleware: "guest-guard"
})
</script>

<template>
  <section>
    <h1 class="text-2xl font-bold">Register</h1>

    <form
      class="mt-10 flex flex-col items-center justify-center space-y-6"
      action="/api/register"
      method="POST"
    >
      <input
        class="rounded-lg border p-3"
        placeholder="Username"
        name="username"
        type="text"
        readonly
        onfocus="this.removeAttribute('readonly')"
      />
      <input
        class="rounded-lg border p-3"
        placeholder="Password"
        name="password"
        type="password"
        readonly
        onfocus="this.removeAttribute('readonly')"
      />
      <button class="rounded-lg border bg-blue-500 p-3 text-white">
        Register
      </button>
    </form>
    <hr class="my-5" />
    <p class="text-center text-gray-600">
      <NuxtLink to="/login" class="text-blue-500 underline">
        Already registered?
      </NuxtLink>
    </p>
    <hr class="my-5" />
  </section>
</template>

// app/pages/login.vue

<script setup lang="ts">
definePageMeta({
  middleware: "guest-guard"
})
</script>

<template>
  <section>
    <h1 class="text-2xl font-bold">Login</h1>

    <form
      class="mt-10 flex flex-col items-center justify-center space-y-6"
      action="/api/login"
      method="POST"
      autocomplete="off"
    >
      <input
        class="rounded-lg border p-3"
        placeholder="Username"
        name="username"
        type="text"
        readonly
        onfocus="this.removeAttribute('readonly')"
      />
      <input
        class="rounded-lg border p-3"
        placeholder="Password"
        name="password"
        type="password"
        readonly
        onfocus="this.removeAttribute('readonly')"
      />
      <button class="rounded-lg border bg-blue-500 p-3 text-white">
        Login
      </button>
    </form>
    <hr class="my-5" />
    <p class="text-center text-gray-600">
      <NuxtLink to="/register" class="text-blue-500 underline">
        New User?
      </NuxtLink>
    </p>
    <hr class="my-5" />
  </section>
</template>
  • definePageMeta helps us declare a route guard and runs first on the server.

Change Password

Just another form with opposite route guard.

<script setup lang="ts">
definePageMeta({
  middleware: "auth-guard",
})
</script>

<template>
  <section>
    <h1 class="text-2xl font-bold">Change Password</h1>

    <form
      class="mt-10 flex flex-col items-center justify-center space-y-6"
      action="/api/password"
      method="POST"
    >
      <input
        class="rounded-lg border p-3"
        placeholder="Current Password"
        name="current_password"
        type="password"
        readonly
        onfocus="this.removeAttribute('readonly')"
      />
      <input
        class="rounded-lg border p-3"
        placeholder="New Password"
        name="new_password"
        type="password"
        readonly
        onfocus="this.removeAttribute('readonly')"
      />
      <button class="rounded-lg border bg-blue-500 p-3 text-white">
        Change Password
      </button>
    </form>
  </section>
</template>

Dashboard

Our dashboard page can only be viewed when logged in, shows the user, and links to the change password page.

<script setup lang="ts">
definePageMeta({
  middleware: ["auth-guard"]
})
const { userId } = await useAuth()
</script>

<template>
  <section class="space-y-6">
    <h1 class="text-2xl font-bold">Dashboard</h1>
    <p>This will be your login wall dashboard!</p>
    <p>
      Your user ID is: <span class="font-mono font-bold">{{ userId }}</span>
    </p>
    <hr class="my-5" />
    <p class="text-center text-gray-600">
      <NuxtLink to="/password" class="text-blue-500 underline">
        Change Password
      </NuxtLink>
    </p>
    <hr class="my-5" />
  </section>
</template>

We can have many guards in an array if necessary.

Usage

We log in to access the dashboard, then register first.

Home - Welcome to the Nuxt Surreal Auth Example

Login - username, password, new user?

Register - username, password, already registered?

Final Notes

I did not create a demo for this, as I didn’t want to pay for a new cloud instance.

Security

The login token only lasts 30 minutes and is not 100% secure for production apps. If you have a small app where you’re not worried about a token getting stolen, this is a login method for you.

Otherwise, use Surreal in conjunction with better-auth or firebase-auth. However, it should be good for any startup application.

Error Handling

The REST API endpoints throw errors. The error handling should be handled best in the forms. I didn’t include this as this app was already getting too complex. However, I do recommend handling this in a production app.

Repo: GitHub

Happy Nuxting!

Read the whole story
alvinashcraft
8 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories