ReactJS/AWS Cognito getting user input mid-execution - javascript

I have a simple login page built with React that uses the AWS Cognito API to authenticate the user. There are some authentication scenarios (password needs to be updated, need to enter an MFA code, etc.) that require me to get user input mid-execution of the authenticateUser workflow. I'm trying to find a way to get the user input dynamically without using the built-in prompt() method, especially when a user is entering a new password. Based on how the authenticateUser workflow is structured, I'm trying to get all user input within the workflow.
Perhaps I'm not thinking about this problem in the right way, but how can I have another React component dynamically render, get user input (new password, MFA code, etc), and then use that input within the authenticateUser workflow?
The main Login component has a form that upon clicking the Submit button triggers the following function:
handleSubmit = async (event) => {
event.preventDefault();
this.setState({ isLoading: true, loginError: null });
try {
await this.login(this.state.username, this.state.password);
this.props.userHasAuthenticated(true);
}
catch(e) {
//alert(e);
this.setState({ isLoading: false, loginError: e.toString() });
}
}
And then we have the login function that goes through the authenticateUser workflow:
login(username, password) {
const userPool = new CognitoUserPool({
UserPoolId: config.cognito.USER_POOL_ID,
ClientId: config.cognito.APP_CLIENT_ID
});
const authenticationData = {
Username: username,
Password: password
};
const user = new CognitoUser({ Username: username, Pool: userPool });
const authenticationDetails = new AuthenticationDetails(authenticationData);
return new Promise((resolve, reject) => (
user.authenticateUser(authenticationDetails, {
onSuccess: (result) => {
// User authentication was successful
resolve();
},
onFailure: (err) => {
var error = err.toString();
// If password expired
if (error.includes('Password reset required for the user')) {
var verificationCode = prompt('Password reset required. Please enter the verification code sent to your trusted device.' ,'');
var newPassword1 = '';
var newPassword2 = '';
while (newPassword1 !== newPassword2 || newPassword1.length < 8) {
newPassword1 = prompt('Please enter a new password.','');
newPassword2 = prompt('Please confirm your new password','');
}
user.confirmPassword(verificationCode, newPassword1, {
onSuccess: (result) => {
//Not sure if this handleSubmit does anything
//this.handleSubmit;
this.setState({ loginError: 'Password updated successfully! Please login with new password.', loginAlert: "success", updatePasswordUpdateAttribute: true });
return;
},
onFailure: (err) => {
this.setState({ loginError: err.toString() });
reject(err)
}
});
}
// User authentication was not successful
console.error(err);
reject(err)
},
mfaRequired: (codeDeliveryDetails) => {
// MFA is required to complete user authentication.
// Get the code from user and call
var verificationCode = prompt('Please enter the multi-factor code sent to your trusted device.' ,'');
user.sendMFACode(verificationCode, this);
},
newPasswordRequired: (userAttributes, requiredAttributes) => {
// User was signed up by an admin and must provide new
// password and required attributes, if any, to complete
// authentication.
// Get these details and call
// newPassword: password that user has given
// attributesData: object with key as attribute name and value that the user has given.
user.completeNewPasswordChallenge(newPassword1, null, {
onSuccess: (result) => {
this.updatePasswordAttribute(user);
resolve()
},
onFailure: (err) => {
this.setState({ loginError: err.toString() });
reject(err)
}
})
}
})
));
}

Related

User object is getting populated but Auth.currentSession is returning "No user found"

When the user clicks on the "sign-in" button and if user.challangeName === 'NEW_PASSWORD_REQUIRED'is true, I redirect the user to a page (form screen) where he can provide the input for required attributes and a new password. Even tho the user object is getting populated upon clicking on the sign-in button, using Auth.currentsession on the form screen will print "No user found"
Can someone let me know why I'm seeing no user? What am I doing wrong here?
Here's my login function (triggered when clicked on sign-in button) where I direct the user to the change password screen (form screen) if user.challangeName === 'NEW_PASSWORD_REQUIRED' is true.
const login = async (email, password) => {
try {
const user = await Auth.signIn(email, password);
if (user.challengeName === 'NEW_PASSWORD_REQUIRED') {
navigate('/change-password');
return;
}
if (user) {
setToken(user.signInUserSession.idToken.jwtToken);
const userDetails = await getAccountDetails();
dispatch({
type: LOGIN,
payload: {
user: {
attributes: user.attributes,
username: user.username
},
client: userDetails
}
});
}
} catch (error) {
await logout();
throw error;
}
};
Here's my onSubmit function on the change password screen where eventually I want to use Auth.completeNewPassword to update the user's password in Cognito
const onSubmitClick = (e) => {
e.preventDefault();
if (validateFields());
Auth.currentSession()
.then((user) => console.log(user))
.catch((err) => console.log(err));
};
Here's the documentation provided by AWS https://docs.amplify.aws/lib/auth/manageusers/q/platform/js/#forgot-password, and the code provided by AWS to update password
Auth.signIn(username, password)
.then(user => {
if (user.challengeName === 'NEW_PASSWORD_REQUIRED') {
const { requiredAttributes } = user.challengeParam; // the array of required attributes, e.g ['email', 'phone_number']
Auth.completeNewPassword(
user, // the Cognito User Object
newPassword, // the new password
// OPTIONAL, the required attributes
{
email: 'xxxx#example.com',
phone_number: '1234567890'
}
).then(user => {
// at this time the user is logged in if no MFA required
console.log(user);
}).catch(e => {
console.log(e);
});
} else {
// other situations
}
}).catch(e => {
console.log(e);
});
New updated answer to reflect your updated post, change the onSubmitClick to the following:
const onSubmitClick = (e) => {
e.preventDefault();
if (validateFields());
Auth.currentAuthenticatedUser()
.then(user => {
console.log(user))
})
.then((data) => console.log(data))
.catch((err) => console.log(err));
};
You are looking at the documentation which shows how to use the user after Auth.signIn() (which was my previous answer), compared to what you use: Auth.currentAuthenticatedUser(). The proper documentation example you have to look at is this one: https://www.docs.amplify.aws/lib/auth/manageusers/q/platform/js

how to hide my client's password in API response

I am just a beginner at Javascript & MERN. I am trying to create a small social media app, and in my sign up api, I gave a response of the user's info. I couldn't segregate and hide the password.
here is the code
userRouter.post("/signUp", async (req, res) => {
const {name, userName, email, password} = req.body
const existingUser = await userSchema.findOne({email: email})
const SameUserName = await userSchema.findOne({userName: userName})
if (existingUser) {
return res.status(406).send({
message: `sorry, an account with email: ${email} has already been created.`
})
} else if (SameUserName) {
return res.status(406).send({
message: `sorry, user name taken. Try another one...`
})
}
const newUser = new userSchema({
name,
userName,
email,
password
})
console.log(newUser)
try {
await newUser.save()
res.status(201).send({
message: `Account successfully created!`,
user: newUser
})
} catch (err) {
res.send({
message:`Something went wrong`,
})
}
})
So, how can I send the user info without the password?
Following up on the comment I left below, here is what you can do.
Refactoring of your code is must thou.
try {
const userSaved = await newUser.save();
delete userSaved.password // assuming this is the property name
return res.status(201).send({ message: 'Account created successfully', user: userSaved })
}
you could also just:
try {
const userSaved = await newUser.save();
delete userSaved.password // assuming this is the property name
return userSaved;
}
In this case you handle the message and everything on the front-end.
You'll want to implement the toJSON and transform methods on your schema. This will allow you to 'intercept' schema objects as they are created, and as they are serialized and sent to the client.
Here's an example:
Schema:
import { Schema, model } from 'mongoose';
const schema = new Schema(
{
name: {
required: true,
type: String
},
userName: {
required: true,
type: String
},
email: {
required: true,
type: String
},
password: {
required: true,
type: String
}
},
{
// here, we implement the `toJSON` method to serialize the user object sans password, __v;
// we'll also convert the mongo-specific `_id` property to a db-agnostic format
toJSON: {
transform(_, ret) {
ret.id = ret._id;
delete ret.password;
delete ret._id;
delete ret.__v;
}
}
}
);
// this is our user schema, used to initialize new user objects before we persist them in the db
const User = model('User', schema);
userRouter.post('/signUp', async (req, res) => {
// grab the inputs - we do *not* at this time know whether any of these are valid - they must be validated
const { name, userName, email, password } = req.body;
// validate the email format, performing checks for any requirements you wish to enforce
if (!email) {
// error response
}
// now, we check if the email is already in-use
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).send({
message: `sorry, an account with email: ${email} has already been created.`
});
}
// validate userName format here
if (!userName) {
// error response
}
// notice we don't bother making this query until `existingUser` check has passed
// this way we don't incur needless computation
const sameUserName = await User.findOne({ userName });
if (sameUserName) {
return res.status(400).send({
message: `sorry, user name taken. Try another one...`
});
}
// validate name and password and handle accordingly here
if (!name || ...) {
// error response
}
// assuming all is well, we create a new user with the schema
// think of the schema as a template
const newUser = new User({ name, userName, email, password });
// save the new user
await newUser.save().catch((ex) => {
// error response
});
res.status(201).send({
message: `Account successfully created!`,
user: newUser
});
});
You might also look into express-validator, a middleware that handles much of the request body validation for you.

Make a login and registration system using Azure functions and Azure SQL server

I want to make a Dating application using node.js and javascript with Azure functions and an Azure sql server. I can create a user so it appears in my database, but how do I make a login system that "checks" if the users email and password is in the database and is correct.
This is what I have so far:
**Login.js:**
var form = document.getElementById("form")
form.addEventListener('submit', function(e) {
e.preventDefault()
var email = document.getElementById("email").value
var password = document.getElementById("password").value
fetch("http://localhost:7071/api/login", {
method: 'POST',
body: JSON.stringify({
email: email,
password: password,
}),
headers: {
"Content-Type": "application/json; charset-UTF-8"
}
})
.then((response) => {
return response.text()
})
.then((data) => {
console.log(data)
}).catch((err) =>{ // catcher fejl, hvis noget går galt
console.log("wuups: " + err)
})
})
**DB.js connect:**
function login (payload) {
return new Promise((resolve, reject) => {
const sql = 'SELECT * FROM [user] where email = #email AND password = #password'
const request = new Request(sql,(err,rowcount) =>{
if (err){
reject(err)
console.log(err)
} else if( rowcount == 0){
reject({messsage:"user does not exit"})
}
});
request.addParameter('email', TYPES.VarChar, payload.email)
request.addParameter('password', TYPES.VarChar, payload.password)
request.on('row',(colums) => {
resolve(colums)
})
connection.execSql(request)
return "you are now logged in"
});
}
module.exports.login = login;
You're on the right track. Consider an updated version of db.sql:
function login(payload, connection) {
return new Promise((resolve, reject) => {
const sql = 'SELECT * FROM [user] where email = #email AND password = #password'
const request = new Request(sql, (err, rowCount) => {
if (err) {
reject(err)
console.error(err)
}
else {
if (rowCount == 1) {
resolve(payload.email)
}
else {
reject('Invalid credentials')
}
}
});
request.addParameter('email', TYPES.VarChar, payload.email)
request.addParameter('password', TYPES.VarChar, payload.password)
connection.execSql(request)
});
}
Since we can infer a successful login from the amount of returned rows, we don't need access to the actual rows in the row callback.
However: as pointed out by Robert in the comments, storing passwords in plain text is a security concern (since access to the database immediately unveils user passwords).
Better approach
The better approach is to store hashed passwords instead. Imagine this simple user table schema in MSSQL:
CREATE TABLE [User] (
[Email] [varchar](max) NOT NULL UNIQUE,
[PasswordHash] [varchar(max)] NOT NULL
)
The login procedure will remain almost the same. Instead of comparing passwords we now compare hashed passwords. Without going into too much detail, you would usually use a library for this purpose (to handle salts, mitigate timing attacks, etc.). I chose bcryptjs for the example below:
var bcrypt = require('bcryptjs');
function login(email, password, connection) {
return new Promise((resolve, error) => {
const sql = 'SELECT * FROM [user] where email = #email' // Note that the password comparison no longer lives here
const request = new Request(sql, (err, rowCount) => {
if (err) {
reject(err)
}
})
request.addParameter('email', TYPES.VarChar, email)
let userRow = null
// This time we need the 'row' callback to retrieve the password hash
request.on('row', row => {
userRow = {
email = row[0].value,
passwordHash = row[1].value
}
})
// .. and the 'done' callback to know, when the query has finished
request.on('done', rowCount => {
if (rowCount == 0) {
reject('User not found')
}
else {
bcrypt.compare(password, userRow.passwordHash) // Password comparison
.then(passwordsMatch => {
if (passwordsMatch) {
resolve(email)
}
else {
reject('Invalid credentials')
}
})
}
})
connection.execSql(request)
})
}
And here's an example of how to create new users with this approach using the same library:
var bcrypt = require('bcryptjs');
const PASSWORD_SALT_ROUNDS = 10 // Learn more at ex. https://stackoverflow.com/questions/46693430/what-are-salt-rounds-and-how-are-salts-stored-in-bcrypt
function createNewUser(email, password, connection) {
return bcrypt.hash(password, PASSWORD_SALT_ROUNDS).then(passwordHash => {
const sql = 'INSERT INTO [user] (Email, PasswordHash) VALUES (#email, #passwordHash)'
const request = new Request(sql, err => {
if (err) {
error(err)
}
else {
resolve()
}
})
request.addParameter('Email', TYPES.VarChar, email)
request.addParameter('PasswordHash', TYPES.VarChar, passwordHash)
connection.execSql(request)
})
}
Consider this a pragmatic proposal to get started. Please note, that the code is illustrative, since I haven't actually executed it, and it is made under certain assumptions.

Forgot password functionality using NodeJs/Knex/Nodemailer and it is not working properly

Note: this is my first time posting, if you have feedback please let me know
Goal: I am building some endpoints that let a user reset their password if they forgot it. Flow would look like this:
User doesn't know password so they click on forgot password.
User types in email and clicks send
User receives email with link to reset password. Clicks on link and is redirected to type in their new password.
They click 'save' and they are redirected to login to sign in with their new password
I am using Insomnia to hit the endpoints for testing.
Things that are working:
When providing an email to reset password, Nodemailer does send out an email.
When updating the password it does show 'password updated' and gives a 200 status.
Bugs:
After trying to log in with that new password, it is not saving to the database. Only the old password will allow you to log back in.
Things I have tried:
I tried changing my user.model to use my findByEmail function and ran into some weird bugs, which then led me down a rabbit hold of issues.
I tried console logging quite a few things to see if I could trace the path.
I tried changing the user.update function but was not able to get it to work.
Here is my code:
Any guidance would be appreciated. If you need to look at any other files please let me know.
Forgot.password.js
const router = require('express').Router();
const crypto = require('crypto')
const User = require('../models/users.model')
const nodemailer = require('nodemailer')
router.post('/forgotpassword', (req, res) => {
let {
email
} = req.body
console.log(req.body)
// if (req.body.email === '') {
// res.status(400).json({ message: 'Email is required'})
// } console.error(req.body.email)
User.findBy({
email
})
.first()
.then(user => {
if (user === null) {
res.status(403).json({
message: 'Email not in db'
})
} else {
const token = crypto.randomBytes(20).toString('hex')
User.update({
resetPasswordToken: token,
resetPasswordExpires: Date.now() + 3600000,
})
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: `${process.env.EMAIL_USER}`,
pass: `${process.env.EMAIL_PASS}`
}
})
const mailOptions = {
from: `${process.env.EMAIL_USER}`,
to: `${user.email}`,
subject: '[Promoquo] Reset Password Link',
text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
'Please click on the following link, or paste this into your browser to complete the process within one hour of receiving it:\n\n' +
`http://localhost:5000/reset/${token}\n\n` +
'If you did not request this, please ignore this email and your password will remain unchanged.\n',
}
transporter.sendMail(mailOptions, (err, res) => {
if (err) {
console.log('ERROR coming from forgot.password js and it sucks', err)
} else {
console.log('here is the res', res)
res.status(200).json({
message: 'recovery email sent hell yes'
})
}
})
}
res.status(200).json({
message: 'Reset password email has been sent WOOHOO 🎉'
})
})
.catch(error => {
res.status(500).json({
message: 'ERROR on last catch forgotpassword.js, likely no user exists',
error
})
console.log(error)
})
})
module.exports = router
Update.password.js
const router = require('express').Router();
const passport = require('passport')
const bcrypt = require('bcrypt')
const User = require('../models/users.model')
const BCRYPT_SALT_ROUNDS = 12
router.put('/updatePasswordViaEmail', (req, res) => {
User.find({
where: {
username: req.body.username,
resetPasswordToken: req.body.resetPasswordToken,
resetPasswordExpires: Date.now() + 3600000,
}
})
.then(user => {
if (user == null) {
console.error('password reset link has expired')
res.status(403).json({ message: 'Password reset link is invalid or has expired' })
} else if (user != null) {
console.log('user exists in db')
bcrypt.hash(req.body.password, BCRYPT_SALT_ROUNDS)
.then(hashedPassword => {
User.update({
password: hashedPassword,
resetPasswordToken: null,
resetPasswordExpires: null,
})
})
.then(() => {
console.log('log for THEN updating password')
res.status(200).json({ message: 'password updated' })
})
} else {
console.error('no user exists in db to update')
res.status(401).json({ message: 'no user exists in db to update'})
}
})
})
module.exports = router
Users.model.js
const db = require('../dbConfig')
module.exports = {
add,
find,
findBy,
findById,
findByEmail,
findByType,
update
};
function find() {
return db('users').select('id', 'username', 'email', 'password');
}
function findBy(filter) {
return db('users').where(filter);
}
async function add(user) {
const [id] = await db('users').insert(user);
return findById(id);
}
function findById(id) {
return db('users').where({ id }).first();
}
function findByEmail(email) {
return db('users').where({ email }).first();
}
function findByType(type) {
return db('users').where({ type }).first();
}
function update(changes, id) {
return db('users').where({ id }).update(changes)
}
20200913211559_users.js (this is the table)
exports.up = function(knex) {
return knex.schema.createTable('users', tbl => {
tbl.increments();
tbl.string('firstname', 30).notNullable();
tbl.string('lastname', 30).notNullable();
tbl.string('username', 30).notNullable()
tbl.string('email', 50).notNullable()
tbl.string('password', 128).notNullable();
tbl.string('type').notNullable();
tbl.boolean('confirmed').defaultTo('false');
tbl.string('resetPasswordToken');
tbl.date('resetPasswordExpires');
})
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('users')
};
Your User.update() lines aren't running (you either need to return their promises into the chains of promises, or hook into their callbacks). async/await is your friend here to avoid "callback hell."
const user = await User.find({
where: {
username: req.body.username,
resetPasswordToken: req.body.resetPasswordToken,
resetPasswordExpires: Date.now() + 3600000,
}
})
if (!user) { /* ... */ }
const token = crypto.randomBytes(20).toString('hex')
await User.update({ // await here!
resetPasswordToken: token,
resetPasswordExpires: Date.now() + 3600000,
})

How to allow my user to reset their password on Cognito User Pools?

So in my app I obviously want to provide the means for users to reset their passwords. The issue I'm having though is that the new documentation for User Pools is pretty ambiguous on this topic. Here is what they tell you to do for a Forgot Password flow, and the link you may find it at:
cognitoUser.forgotPassword({
onSuccess: function (result) {
console.log('call result: ' + result);
},
onFailure: function(err) {
alert(err);
},
inputVerificationCode() {
var verificationCode = prompt('Please input verification code ' ,'');
var newPassword = prompt('Enter new password ' ,'');
cognitoUser.confirmPassword(verificationCode, newPassword, this);
}
});
http://docs.aws.amazon.com/cognito/latest/developerguide/using-amazon-cognito-user-identity-pools-javascript-examples.html
However when I drop this code into my project where a cognitoUser is defined and signed in, nothing seems to happen. I understand I need to somehow integrate this code with sending a verification code to the user, and asking them for a new password, but can't find anything on how to do this. Thoughts?
Thanks
AWS' docs are terrible on this topic (Cognito). You basically need to setup cognitoUser, then call forgotPassword
export function resetPassword(username) {
// const poolData = { UserPoolId: xxxx, ClientId: xxxx };
// userPool is const userPool = new AWSCognito.CognitoUserPool(poolData);
// setup cognitoUser first
cognitoUser = new AWSCognito.CognitoUser({
Username: username,
Pool: userPool
});
// call forgotPassword on cognitoUser
cognitoUser.forgotPassword({
onSuccess: function(result) {
console.log('call result: ' + result);
},
onFailure: function(err) {
alert(err);
},
inputVerificationCode() { // this is optional, and likely won't be implemented as in AWS's example (i.e, prompt to get info)
var verificationCode = prompt('Please input verification code ', '');
var newPassword = prompt('Enter new password ', '');
cognitoUser.confirmPassword(verificationCode, newPassword, this);
}
});
}
// confirmPassword can be separately built out as follows...
export function confirmPassword(username, verificationCode, newPassword) {
cognitoUser = new AWSCognito.CognitoUser({
Username: username,
Pool: userPool
});
return new Promise((resolve, reject) => {
cognitoUser.confirmPassword(verificationCode, newPassword, {
onFailure(err) {
reject(err);
},
onSuccess() {
resolve();
},
});
});
}
Resetting the password with forgot password flow has two steps:
Start the process by requesting for a verification code from the service. A code will be delivered to the user's phone/email.
Set the new password using the delivered verification code.
Use these two functions to perform the above steps and reset the password:
cognitoUser.forgotPassword(): This will start the forgot password process flow. The service generates a verification code and sends it to the user. The "data", returned through callback.inputVerificationCode(data), indicates where the verification code was sent.
cognitoUser.confirmPassword(): Use the delivered verification code with this function to set a new password.
I had this same issue. Was able to work through it by using confirmPassword() in the following way.
//validation of input from form
req.checkBody('email', 'Username is required').notEmpty();
req.checkBody('password', 'Password is required').notEmpty();
req.checkBody('confirmationcode', 'Confirmation Code is required').notEmpty();
var confirmationCode = req.body.confirmationcode;
var password = req.body.password;
var userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
var userData = {
Username: req.body.email,
Pool: userPool
};
var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
cognitoUser.confirmPassword(confirmationCode, password, {
onFailure(err) {
console.log(err);
},
onSuccess() {
console.log("Success");
},
});
If as me, you find how to handle this case with amplify
import { Auth } from 'aws-amplify';
// Send confirmation code to user's email
Auth.forgotPassword(username)
.then(data => console.log(data))
.catch(err => console.log(err));
// Collect confirmation code and new password, then
Auth.forgotPasswordSubmit(username, code, new_password)
.then(data => console.log(data))
.catch(err => console.log(err));
See https://docs.amplify.aws/lib/auth/manageusers/q/platform/js#forgot-password
So Even I faced a same issue, Even in AWS cognito documentation it was not clear, basically the process involves two steps.
call cognitoUser.forgotPassword() this will start forgot password process flow, and the user will receive a verification code.
then call cognitoUser.confirmPassword() which will reset the password verifying the code send to the email of user.
Below I have given a cognitoUserClass(Typescript) which has static methods forgotPassword() and confirmPassword() methods which implements those two steps.
import * as AmazonCognitoIdentity from 'amazon-cognito-identity-js'
class cognitoUserClass {
static cognitouser: AmazonCognitoIdentity.CognitoUser
static userPool = new AmazonCognitoIdentity.CognitoUserPool({
UserPoolId: 'your pool id',
ClientId: 'your client id',
})
static forgotPassword(userName: string): void {
const userData = {
Username: userName,
Pool: cognitoUserClass.userPool,
}
cognitoUserClass.cognitouser = new AmazonCognitoIdentity.CognitoUser(
userData
)
cognitoUserClass.cognitouser.forgotPassword({
onSuccess: (data) => {
console.log(data)
},
onFailure: (err) => {
console.log('ERR:', err)
},
})
}
static confirmPassword(
verificationCode: string,
newPassword: string
): void {
cognitoUserClass.cognitouser.confirmPassword(
verificationCode,
newPassword,
{
onFailure(err) {
console.log(err)
},
onSuccess(data) {
console.log(data)
},
}
)
}
}
export { cognitoUserClass }
After you've got the verification code, using aws-amplify it's as easy as follows
import { Auth } from "aws-amplify";
Auth.forgotPasswordSubmit(email, verificationCode, newPassword)
.then(() => {
//redirect to sign-in page
})
.catch(error => {
//error logic
})

Categories

Resources