We will have 2 classes, Authenticator
and User
. Authenticator will be used for login and sending OTP mail. User will be used for keep user info for each user. Lets begin.
Authenticator Class
Authenticator will get an email on initial payload. On init we will save that email to state. And sendOTP will generate otp and send it to the email from state.
Dependencies
We will use mailjet
to send otp mails and zod
for models.
"node-mailjet": "5.1.1",
"zod": "3.19.1"
Models
Turn this zod models into Rio models by following instructions on this link.
// classes/Authenticator/types.ts
import { z } from 'zod'
export const initInputModel = z.object({
email: z.string().email()
})
export const loginModel = z.object({
otp: z.number()
})
// Optional
export const privateState = z.object({
email: z.string().email(),
otp: z.number()
})
export type InitInputModel = z.infer<typeof initInputModel>
export type Login = z.infer<typeof loginModel>
export type PrivateState = z.infer<typeof privateState>
Template.yml
You have to convert zod models above to rio models to use as an input model. As you can see we have models for init
and login
function.
# classes/Authenticator/template.yml
init:
handler: index.init
inputModel: AuthInitInputModel
getInstanceId: index.getInstanceId
getState: index.getState
methods:
- method: login
type: WRITE
inputModel: LoginModel
handler: index.login
- method: sendOTP
type: WRITE
handler: index.sendOTP
Index.ts
We will use mailjet
to send mails. So let's configure mailjet
.
// classes/Authenticator/index.ts
import mailjet from 'node-mailjet'
const mailjetClient = new mailjet({
apiKey: 'YOUR_API_KEY',
apiSecret: 'YOUR_API_SECRET'
});
Init Function
We are getting email here and saving it to state, later we will get email from state.
// classes/Authenticator/index.ts
export async function init(data: Data): Promise<Data> {
data.state.private.email = data.request.body.email
return data
}
GetInstanceId Function
Making instanceId same as email, this way we will not create a billion instances.
// classes/Authenticator/index.ts
export async function getInstanceId(data: Data): Promise<string> {
return data.request.body.email
}
sendOTP Function
Then lets create a function that handles sending mails. This function gets email from state. And generating a six digit otp.
// classes/Authenticator/index.ts
export async function sendOTP(data: Data): Promise<Data> {
try {
const { email } = data.state.private
const otp = Math.floor(100000 + Math.random() * 900000)
await mailjetClient
.post("send", { 'version': 'v3.1' })
.request({
"Messages": [
{
"From": {
"Email": "bahadir@rettermobile.com",
"Name": "Bahadır"
},
"To": [
{
"Email": email
}
],
"Subject": "Greetings from Retter.",
"TextPart": "OTP Validation Email",
"HTMLPart": `OTP: ${otp}`,
"CustomID": "AppGettingStartedTest"
}
]
})
data.state.private.otp = otp
data.response = {
statusCode: 200,
body: { emailSent: true },
}
} catch (error) {
console.log(error)
data.response = {
statusCode: 400,
body: { error: error.message },
};
}
return data
}
Login Function
First checking if received otp is a match. If not just throws an error.
// classes/Authenticator/index.ts
const { otp: recivedOtp } = data.request.body as Login
const { otp, email } = data.state.private as PrivateState
if (recivedOtp !== otp) throw new Error(`OTP is wrong`);
After checking otp
We are looking for an instance in User
class with our email lookup key. If can't find it we are initializing a new instance. Checking if maybe initialization failed.
Giving email inside body because when user class initializing it will set email
lookup key with this email and we will keep that email in state.
// classes/Authenticator/index.ts
// Get existing USER INSTANCE
let getUser = await rdk.getInstance({
classId: "User",
body: {
email
},
lookupKey: {
name: "email",
value: email
}
})
if (getUser.statusCode > 299) {
// CREATE USER INSTANCE -> User class will connect email as lookup key itself
getUser = await rdk.getInstance({
classId: "User",
body: {
email
}
})
if (getUser.statusCode > 299) throw new Error('Couldnt create user instance')
}
After that, generating custom token and Using instanceId
as userId
and returning it inside body.
// classes/Authenticator/index.ts
const customToken = await rdk.generateCustomToken({
userId: getUser.body.instanceId,
identity: 'enduser'
})
data.state.private.relatedUser = getUser.body.instanceId
data.state.private.otp = undefined
data.response = {
statusCode: 200,
body: customToken
};
Authorizer Function
Here we are allowing init
and get
. Allowing getting state if developer because state keeps ours otp. For login
and sendOTP
checking if we have instanceId
, æction
is "CALL"
.
// classes/Authenticator/index.ts
export async function authorizer(data: Data): Promise<Response> {
const { methodName, identity, instanceId, action } = data.context
switch (methodName) {
case 'INIT':
case 'GET': {
return { statusCode: 200 }
}
case 'STATE': {
if (identity === 'developer') return { statusCode: 200 }
}
case 'login':
case 'sendOTP': {
if (instanceId && action === 'CALL') return { statusCode: 200 }
}
}
return { statusCode: 403 };
}
User Class
Dependencies
I used uuid
library to generate userId's. Instance id's will be created with this.
"uuid": "9.0.0",
"zod": "3.19.1"
Models
Turn this zod models into Rio models by following instructions on this link.
// classes/User/types.ts
import { z } from 'zod'
export const privateState = z.object({
email: z.string().email(),
userId: z.string()
})
export const userInitModel = z.object({
email: z.string().email()
})
export type PrivateState = z.infer<typeof privateState>
export type UserInitModel = z.infer<typeof userInitModel>
Template.yml
You have to convert zod models above to rio models to use as an input model. We have input model for init
function here.
# classes/User/template.yml
init:
handler: index.init
inputModel: UserInitInputModel
getState: index.getState
getInstanceId: index.getInstanceId
methods:
- method: getProfile
type: READ
handler: index.getProfile
Index.ts
GetInstanceId Function
Generating userId and making it instance id.
// classes/User/index.ts
import { v4 as uuidv4 } from 'uuid';
export async function getInstanceId(): Promise<string> {
return uuidv4()
}
Init Function
Saving email and userId to state. And setting lookup key so we can find this instance with email.
// classes/User/index.ts
export async function init(data: Data): Promise<Data> {
const { email } = data.request.body as UserInitModel
data.state.private = {
email,
userId: data.context.instanceId
} as PrivateState
await rdk.setLookUpKey({ key: { name: 'email', value: email } })
return data
}
GetProfile Function
This function just returns state because user data is in state.
// classes/User/index.ts
export async function getProfile(data: Data): Promise<Data> {
data.response = {
statusCode: 200,
body: data.state.private,
};
return data;
}
Authorizer Function
State keeps user data so we don't want everyone to access it. And allowing getProfile if it fulfills the requirements.
// classes/User/index.ts
export async function authorizer(data: Data): Promise<Response> {
const { identity, methodName,instanceId, userId } = data.context
if (identity === "developer" && methodName === "getState") {
return { statusCode: 200 };
}
if (identity === "enduser" && methodName === "getProfile" && userId === instanceId) {
return { statusCode: 200 };
}
return { statusCode: 403 };
}
Here is the all code for the project .
Now you have an idea of how to create an authentication system. Im glad if I helped. Thanks!
Top comments (0)