NexusCLI is a full-stack AI-powered developer workflow platform featuring a conversational CLI agent and web dashboard. It enables secure, structured interaction with LLMs to automate internal development tasks through command-based execution and contextual AI reasoning.
Next.js Authentication microservices express zod commandjs
Cors Express Better-Auth Prisma PostgreSQL Database - Neon DB(Serverless)
- Initialize Next.js
- Setup Shadcn UI
- Initialize server 4. go into the package.json file and change "type" from "commonjs" to "module"
- Installl prisma and prisma client
- Initialize database inside express application(server)
- get db url from neon db
- Make a test migration
- Install Better-auth - Follow docs - #complete 2. implementing a cli auth flow
- Setup cors in your express application - #complete 2. add the client url "localhost:3000" to trusted Origins in [auth.js]
- Setup better auth into express - #complete 2. when setting up better-auth into express, during setting up with the database(postgresql - neon serverless service) better-auth creates the accounts, sessions etc. that need to go into the database 3. use social provider oauth (github) 3. register new oauth app with github(such as homepage url, callback url, device flow) 4. implement better-auth in express 1. simple example of bootstrapping express with better-auth 2. read the caution above before the server.ts
- Make login and Home page ui - For user/device(client) flow
- Setup better-auth in the client as well
- create auth-client.ts in the client folder
- build the authorization in the auth-client file
- inside the app folder, create a group folder called auth
(auth)- inside the
(auth)create a simple segment namedsign-in - in the
(auth)add a layout file - [layout.tsx] - update [layout.tsx] to render only children but center everything and set it to h-screen to only apply the full viewport height to the specific element it is attached to;
- inside [sign-in] create a new file [page.tsx]
- inside the
- Setup better-auth in the client as well
- implement authclient in nextjs - #complete
- inside the login form import the
authclient - Setup dark mode - use shadcn - follow the docs
- Inside the [main app page], we are going to display the currently logged in user
- get the data of the currently logged in user
- to get the data of the currently logged in user we'll need to retrieve their session from
authClient.useSession(). - this async function returns an object and since we just need
{data, isPending}we perform object(json) destructuring to retrieve the data.
const { data, isPending } = authClient.useSession() - to get the data of the currently logged in user we'll need to retrieve their session from
- mark [page.tsx] as "use client" since we're going to be utilizing browser/client side functionalities that do not exist on server side
- if
isPendingis true it means we're waiting for the data, therefore return a spinner.(in other words if it is not pending then we have received the data and can display the results).- In the same document check if,
isPendingis false(meaning it has completed the async call), then check if theuser datahas been retrieved, if it hasn't we need to make sure that ourprotected UIdoes not flash(show for a brief moment), [why it flashes]
- In the same document check if,
- if we're not waiting for the data (after a page reload),
that is if the api request to fetch the data in
sign-inhas been resolved and we haveauthClient.useSession()has returned something, it'll change the state ofisPendingcausing the page to reload sinceisPendingis most likely a useState variable and its value isfalseallowing the code to go to the next function check if thesessionanduserhas been retrieved, if they have not been retrieved, it means there's no user logged in then route the page back to the sign-in page usinguseRouter().push('sign-in'), which can be found on client side code(Client Components), so that the user can log in- make sure to wrap
useRouter().push('sign-in')in a useEffect, so that it'll run after the page has been loaded else Next.js or React will continue rendering thehome pageand since you navigated to thesign-inpage, it'll also render that page, making both pages render simultaneously. - since we're wrapping the navigation functionallity in a
useEffectit'll be the last to run and this means ourprotected UIwill show. Therefore we need to write another safe guard which ensures that if there'sisPendingis false and we don't have any data, which is the last flow above, then wereturn null
- make sure to wrap
- now if we have
sessionanduserdata, then we will display theprotected UI
- get the data of the currently logged in user
- Now in the [sign-in] page, ensure that, if the user is already logged in, they will be navigated to the [main-page]
- Add your
useEffectwhich will contain the safe guard- make sure that your dependency array contains the variables and objects that'll you'll use inside the
useEffect
- make sure that your dependency array contains the variables and objects that'll you'll use inside the
- check if
isPendingis true, if it is return a spinner(same meaning as the previous) - if it's
falsethen check if we have theuserandsessiondata, if we do, then navigate back to the [main page] Caveats:
- Add your
- Ensure that your backend server has been configured with the correct better-auth url, and what is the better-auth url? it is the server url that handles the authentication and it is the url that github will use to resolve the
redirect_uri. - make sure that cors has been setup on the backend server so that it allows the client app(nextjs) to talk with it, if not the connection will be refused on the browser(throwing a cors policy error) and the server
- make sure you've configured the correct callback in the [login-form] for better-auth cause that is what tells github that after all the authentication process return to the client app(nextjs)
- inside the login form import the
- Completion - add some checks to make sure you executed the above correctly
What is device Flow and ? Device flow(OAuth 2.0 Device Authorization Grant) is an authentication method used by apps that that simply do not have browser or ui available(limited input capabilities), unable to easily handle traditional browser-based login. Examples are CLI tools, TVs, gaming consoles, IoT devices etc.
why we use device flow? traditional OAuth flow uses the browser since this is impractical for some devices, device flow splits authentication across two devices, one with limited capabilities example(tv, cli) and then noe with full capabilities(phone, laptop)
- Follow the better-auth for device authorization(device flow)
- After you copy and paste the client plugin.
- execute
npx auth generateand then npx prisma migrate dev
- execute
- install the following
- commander
- boxen
- chalk
- yocto-spinner
- @clack/prompts
- figlet
- open - open any urls from your cli
- zod - input verifications
- inside the lib folder, create another folder called cli
- inside cli, create main.js and a commands folder
- inside main.js paste the following text
#!/usr/bin/env nodeat the top of the file
- Inside the [main.js] file we're going to setup our console UI.
- we'll need to test implementation by setting up a banner in the console
- hello
- we begin by creating a main function and making turning it into an executable command
- inside this function we are
console logginganother function.(in other words, we're passing another function inside theconsole.log(another_function)) - the function that we pass inside is the
chalk.cyan()function. (let's agree that since we know anything ending with()is a function we don't need to repeat function from now on.) - and then inside
chalk.cyan()we pass the object with it's parameters, in this case{font, horizontalLayout} - add a new
commandnamedorbital, with adescriptionandversion. - in the terminal type
chmod +x ./main.jsto make the main.js file executable ^a4ebaf - go to package.json in the server folder and add
"bin": {"orbital": "./src/cli/main.js"} - now you can run the command
npm linkin the terminal
- inside this function we are
- we'll need to test implementation by setting up a banner in the console
- We will now set up the login functionality.
- We start with the loginAction functionality
- Create two new folders inside [cli/commands], namely
aiandauth- create [login.js]
- inside [[login.js]], import the essential packages
- what you need is your URL, CLIENT_ID, CONFIG_DIR, TOKEN_FILE(stored in database)(although better-auth also wants you to store it inside this token file), you need to create them as
constants - create the login commands
- create a zod object
- define the parameters(accepted) by the
zod objectwhich you would like to validateserverUrl- an optional stringclientId- an optional string
- now that we have validated the parameters we need, it's time to assign them to the respective variables using the data object returned from
zod, which in this case isoptionsand it contains theserverUrlandclientIdattributes.- for each of the variables, if the two optional variables above do not exist, then assign the predefined variables
serverUrl = options.serverUrl || URLclientId = options.clientId || CLIENT_ID
- for each of the variables, if the two optional variables above do not exist, then assign the predefined variables
- what you need is your URL, CLIENT_ID, CONFIG_DIR, TOKEN_FILE(stored in database)(although better-auth also wants you to store it inside this token file), you need to create them as
- when someone selects the action, then
- display an intro
- using chalk: display the text
"Auth Cli Login"in boldchalk.bold(text)takes in the parametertext
intro()takes in the parameters(title, opts, undefined)
- using chalk: display the text
- we need to protect our application, by providing a safeguard that checks if a user's token exists or if the token is expired.
- create a constant
existingTokenwith the valuefalse, this behaviour is such that if a user is already logged in this would betrue - create another variable
expiredwhich will be false - Check - if there's an existing token and it has not expired - then :
- we will display the message
You are already logged in. Do you want to log in again?.- To do this we will create a constant
shouldReAuthand assign it the result of an awaited async functionYouconfirm()which passes in a data object with two attributes ,messageandinitialValue. message: "You are already logged in. Do you want to log in again?"initialValue: false
- To do this we will create a constant
- Check - if the user cancelled the action, by checking the result of
isCancel(shouldReAuth)(isCancel takes in a parameter namedvalue) , or!shouldReAuth(whether there's no shouldReAuth), then- cancel the login steps
- execute
cancel("Login Canceled)
- execute
- and then exit the process by executing
process.exit(0)my question is why aren't we returning but we're exiting the node process
- cancel the login steps
- no new line
- we will display the message
- create a constant
- Create two new folders inside [cli/commands], namely
- we will now complete setting up the
logincommand by exportinglogin- by providing a name for the command.
1. to provide a name we must first create a constant that will store the new command, then initialize a new Command and pass in the name we would like to use inside the initializer function
new Command("login") - by providing a description for the command
1. to provide the "Login to Better Auth" description for the
"login"command, we simply chain another command to the previous initialization by executingnew Command("login").description("Login to Better Auth") - by providing options for the command, this is essentially how you would add the
serverUrland theclientIdto thelogincommand 1. to do this we will chain.option("--server-url <url>", "The Better Auth server URL", URL)to thedescription()function. 2. we will add another chained function.option("--client-id <id>", "The OAuth client ID", CLIENT_ID)3. then, the last function we will chain is.action(loginAction), theloginActionbeing passed is the name of the function we first described. - To register the
logincommand we simply open the file [main.js] and navigate toprogram.version()and chain the commandaddCommand(login)having passed in [login]- To [[#^a4ebaf|turn it into an executable command ]]
- by providing a name for the command.
1. to provide a name we must first create a constant that will store the new command, then initialize a new Command and pass in the name we would like to use inside the initializer function
- Issues faced and changes:
- [[debugs.md#^579408|debbugging]]
- We start with the loginAction functionality