Reactive Form , check if username exists - javascript

I have a problem in Ionic/Firebase with a value of validator in a reactive form. In particular I have this 2 function below that check if a username in a realtime database of firebase exists. The 2 functions return a Promise boolean:
export class UsernameValidator {
static async getSnapshot(fc: FormControl){
let isPresent:boolean = false;
await firebase.database().ref().child("users").orderByChild("username")
.equalTo(fc.value)
.once("value", snapshot => {
}).then((data)=> {
if(data.exists())
isPresent = true;
else
isPresent = false;
});
console.log(isPresent);
return isPresent;
}
static async validUsername(fc: FormControl){
try{
let present:boolean =await UsernameValidator.getSnapshot(fc)
if(present==true)
return (null);
else{
return ({validUsername: true});
}
}catch(e){
console.log(e)
}
}
Then , I have I class in which I define a FormGroup and validator:
constructor(private route: ActivatedRoute, private router: Router,
public pfService: ProfileService, public fb: FormBuilder,
public authService: AuthenticationService)
{
this.id = this.authService.userData.uid;
//Underscore and dot can't be next to each other (e.g user_.name).
//Underscore or dot can't be used multiple times in a row (e.g user__name / user..name).
this.validPattern = "^(?=.{6,20}$)(?!.*[_.]{2})[a-z0-9._]+$";
this.validPatternName = "^[a-z]{3,10}$";
this.userForm = fb.group({
txtUsername: ["",[Validators.required,Validators.pattern(this.validPattern),
UsernameValidator.validUsername]],
txtName: ["",[Validators.required,Validators.pattern(this.validPatternName)]],
});
this.userForm .valueChanges.subscribe(()=> {
console.log(this.userForm.getError('validUsername'))
})
};
The problem is that validUsername , from the console, is always null regardless of the value of isPresent, also when isPresent is false. How can I fix this?

You were close, but you've mixed different syntaxes in your attempts to fix the problem which has lead to confusion.
One other thing that is leading you into trouble, is confusing the two different types of DataSnapshot.
For a direct reference (e.g. database().ref("path/to/data")), you can use exists() and val() to get information about that location's data.
For a queried reference (e.g. database().ref("path/to/group").orderByChild("name").equalTo("Tim's Group")), the data is returned as a list where you can use numChildren() to get the number of matching results, hasChildren() to see if there are any results (similar to exists()) and you can iterate through the results using forEach().
static async isUsernameTaken(fc: FormControl) { // renamed from getSnapshot()
return firebase.database().ref() // note the return here
.child("users")
.orderByChild("username")
.equalTo(fc.value)
.once("value")
.then((querySnapshot) => querySnapshot.hasChildren());
}
However, I do not recommend searching /users for just usernames as it means that your user's data is world-readable and it's also inefficient. Instead you should create an index in your database that contains only usernames.
"/usernames": {
"bob": "userId1",
"frank": "userId2",
"c00lguy": "userId3"
}
If you secure this using these Realtime Database Security Rules, you can also make use of the following simple functions.
{
"usernames": {
"$username": {
// world readable
".read": true,
// must be logged in to edit, you can only claim free usernames OR delete owned usernames
".write": "auth != null && (!newData.exists() || auth.uid == newData.val()) && (!data.exists() || data.val() == auth.uid)",
// strings only
".validate": "newData.isString()",
}
}
}
To check if a username is available:
static async isUsernameTaken(fc: FormControl) {
return firebase.database().ref()
.child("usernames")
.child(fc.value)
.once("value")
.then((dataSnapshot) => dataSnapshot.exists());
}
To claim a username (if the write itself fails, assume the username is taken):
static async claimUsername(fc: FormControl) {
const user = firebase.auth().currentUser;
if (!user) {
throw new Error("You need to login first!")
}
return firebase.database().ref()
.child("usernames")
.child(fc.value)
.set(user.uid);
}

Related

Correctly sanitize data for request - Typescript, React

Background
I'm not sure how I should approach sanitizing data I get from a Java backend for usage in a React form. And also the other way around: sanitizing data I get from a form when making a backend request. For frontend/backend communication we use OpenApi that generates Typescript interfaces and API for us from DTOs defined in Java.
Scenario
Example of the Schema in Java:
public enum Pet {
CAT,
DOG
}
#Schema(description = "Read, create or update an account")
public class AccountDto {
#NotNull
private Boolean active;
#NotBlank
private String userName;
#NotNull
private Pet preferedPet;
#Nullable
private String bioDescription;
// Constructor and getter/setters skipped
}
Current implementation
Example of the generated Typescript interface:
enum Pet {
CAT,
DOG
}
interface AccountDto {
active: boolean,
userName: string,
preferedPet: Pet,
bioDescription?: string // Translates to: string | undefined
}
Example React.js:
import {getAccount, updateAccount, Pet, AccountDto} from "./api"
export default function UpdateAccount() {
const [formData, setFormData] = useState<AccountDto>({
active: true,
userName: "",
preferedPet: Pet.CAT,
bioDescription: ""
})
useEffect(() => {
async function fetchAccount() {
const response = await getAccount();
// Omitted error handling
setFormData(response.data);
// response.data could look like this:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.DOG,
// bioDescription: null
// }
}
}, [])
async function updateAccountHandler() {
const response = await updateAccount(formData);
// Omitted error handling
// Example formData object:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.CAT,
// bioDescription: ""
// }
}
return (
// All input fields
)
}
Problems
When fetching the account, bioDescription is null. React will throw a warning that a component (bioDescription input) is changing from uncontrolled to controlled.
If by any chance there is a situation where null is set for preferedPet we will get a warning that the select value is not valid.
When updating the account all empty strings should be null. Required for the database and generally cleaner in my opinion.
Questions
1.) I'm wondering how other React users prepare/sanitize their data for usage and requests. Is there a go to or good practice I'm not aware of?
2.) Currently I'm using the following function to sanitize my data. It seems to work and Typescript does not notify me about any type mismatches but I think it should since bioDescription can only be string | undefined and not null.
function sanitizeData<T>(data: T, type: "use" | "request"): T {
const sanitizedData = Object.create({});
for (const [key, value] of Object.entries(data)) {
if (!value && type === "use") {
sanitizedData[key] = "";
} else if (!value && type === "request") {
sanitizedData[key] = null;
} else {
sanitizedData[key] = value;
}
}
return sanitizedData;
}
I have a situation where I'm trying to manually change a prop without using the React setState.
formData.description = null;
At this point Typescript is telling me that null is not possible. That's how I detected that my sanitizer function might not be correct.
Demo
Sandbox - https://codesandbox.io/s/async-cdn-7nd2m?file=/src/App.tsx

Firestore query not fetching latest data

I have made a route guard in Angular, that checks for a certain value in firestore before allowing access to said route.
The component is routed to once a HTTP cloud function has completed. This cloud function creates an object in a firestore document called agreement, which the route then listens to the status within this object, to decide whether to allow access or not. The issue we are having though, is that maybe 9/10 times, the guard seems to think that the agreement object does not exist in the document, and therefore throws an error. (Though I can see the agreement object there in firestore!).
I have logged the flow, and it correctly logs within the subscription, then within the guard, so I know that the HTTP function is completing before trying to route and calling the guard class.
This is the guard:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
this.tenancyOffer$ = this._tenancyOffer.getTenancyOfferById(route.params.offerId).get({source: 'server'})).valueChanges();
return this.tenancyOffer$.pipe(
map((tenancyOffer) => {
if (tenancyOffer.status === 'incomplete'
&& tenancyOffer.agreement.status === 'incomplete') {
return true;
} else {
this._router.navigate(['/' + Landlord.base, Landlord.manage.base, Landlord.manage.applications.base, route.params.propId, Landlord.manage.applications.summary, route.params.offerId]);
}
})
);
}
GetTenancyOfferById:
this.tenancyOfferCollection = afs.collection<any>('tenancy_offers');
getTenancyOfferById(id: string) {
return this.tenancyOfferCollection.doc(id);
}
the error is coming from this part: tenancyOffer.agreement.status === 'incomplete', saying TypeError: Cannot read property 'status' of undefined.
The HTTP function I'm calling is here, with the routing to this component in the subscribe:
landlordAcceptReferences() {
this.hasAcceptedRefs = true;
this.processingAcceptRefs = true;
this._references.landlordAcceptReferences(this.offerId).pipe(
finalize(() => {
this.processingAcceptRefs = false;
})
).subscribe(e => {
console.log({e});
this.dref.close();
this._toastr.success(`You have accepted tenant references`);
this._router.navigate(['../../', Landlord.manage.applications.agreement.base, this.offerId], {relativeTo: this._activatedRoute});
}, () => this.hasAcceptedRefs = false);
}
I made the response of the cloud function the entire firestore document, and that HAS the agreement object within it which is logged console.log({e});. I then log (AFTER) in the guard, the value of the document, and it does not exist, even though this is where I make another call to firestore?
Does anyone have any ideas why the getTenancyOfferById(route.params.offerId) function within the guard, is not returning the latest version of this document from firestore, even though the cloud function needs to end before routing in the subscription?
I guess this.tenancyOfferCollection = afs.collection<any>('tenancy_offers'); is defined somewhere in your service. If this is true, the collection will be loaded as soon as your application is loaded.
I recommend to store tenancy_offers values in your service and read it from your guard.
Service:
export class YourService {
private tenancyOfferMap: Map<string, any> = new Map();
constructor(
private afs: AngularFirestore,
) {
afs.collection<any>('tenancy_offers').snapshotChanges()
.subscribe( actions => {
actions.map( action => {
this.tenancyOfferMap.set(action.payload.doc.id, action.payload.doc.data());
});
});
}
getTenancyOfferById(id: string) {
return this.tenancyOfferMap.get(id);
}
}
Guard:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const tenancyOffer = this.yourService.getTenancyOfferById(route.params.offerId);
if (tenancyOffer.status === 'incomplete'
&& tenancyOffer.agreement.status === 'incomplete') {
return true;
} else {
return this._router.navigate(['/' + Landlord.base, Landlord.manage.base, Landlord.manage.applications.base, route.params.propId, Landlord.manage.applications.summary, route.params.offerId]);
}
}
It may depend on the fact that your stream does not complete.
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
this.tenancyOffer$ = this._tenancyOffer.getTenancyOfferById(route.params.offerId).get({source: 'server'})).valueChanges()
.pipe(take(1)); // <-- ensure your stream completes in guards
return this.tenancyOffer$.pipe(
map((tenancyOffer) => {
if (tenancyOffer && tenancyOffer.status === 'incomplete'
&& tenancyOffer.agreement.status === 'incomplete') {
return true;
} else {
this._router.navigate(['/' + Landlord.base, Landlord.manage.base, Landlord.manage.applications.base, route.params.propId, Landlord.manage.applications.summary, route.params.offerId]);
return false; // <-- always return something in guards
}
})
);
}

Type-GraphQL: Allow document owner and admin/moderator access with the #Authorized() decorator

Details
I am using type-graphql and are trying to restrict data access or actions for a specific group of users with the #Authorized() decorator and a custom auth checker as a guard. Everything is working and access is blocked/allowed appropriately according to the decorator.
Problem
I want the owner/author of the document to be able to edit/delete it even if its marked with #Authorized(["MODERATOR", "ADMIN"]) optionally I could mark it with something like #Authorized(["OWNER", "MODERATOR", "ADMIN"]) if that makes it easier to implement.
As far as I know I dont have any information as to what Model/document the user is trying to access in the auth checker only the arguments of the mutation. In other words I have the ID of what they want to access but, not the Model it belongs to.
Question
Is there any way I can check if the user owns the document in the auth checker or will I have to mark it as #Authorized() and check if the user owns the document or is an admin/moderator in every mutation/query?
Code
index.d.ts
declare module "type-graphql" {
function Authorized(): MethodAndPropDecorator;
function Authorized(roles: AccessLevels[]): MethodAndPropDecorator;
function Authorized(...roles: AccessLevels[]): MethodAndPropDecorator;
}
types.ts
type AccessLevels = "MODERATOR" | "ADMIN";
authChecker.ts
const authChecker: AuthChecker<{ user: User | null }, AccessLevels> = ({ context }, roles): boolean => {
if (!context.user) {
return false;
}
if (roles.length === 0) {
return true;
}
if (context.user.accessLevel > 0 && roles.includes("MODERATOR")) {
return true;
}
return context.user.accessLevel > 1 && roles.includes("ADMIN");
};
EstablishmentResolver.ts
#Authorized(["MODERATOR", "ADMIN"])
#Mutation(() => EstablishmentType, { description: "Deletes a establishment by ID" })
async deleteEstablishment(#Args() { id }: EstablishmentIdArg): Promise<Establishment> {
const establishment = await Establishment.findOne({ where: { id } });
if (!establishment) {
throw new Error("Establishment does not exist");
}
await establishment.destroy();
return establishment;
}
Try to modify the signature and logic to allow passing a callback into the decorator, that will resolve the "owner" condition:
#ObjectType
class User {
#Authorized({
roles: ["MODERATOR", "ADMIN"],
owner: ({ root, context }) => {
return root.id === context.user.id;
},
})
#Field()
email: string;
}
Be aware that you can't always do that inside the authChecker or middleware as they run before your resolver code, so in case of deleteEstablishment there's no universal way to match establishmentId with user.id to detect if it's an owner or not.

Angular pushing an array of an user object into a subject

I'm developping a single app and at the moment the only good behavior is that I'm getting an user from an API with HttpClient method.
The method is store in a service.
Getting the user is a success but now I want to get a specific array from that user to re-use it by my will.
Should I make another service since this value will be use in 2 components ?
How should I procced to get this array in a var ?
Exemple of user object :
{
firstName: '',
lastName: '',
arrayIWant: []
}
My user is in a subject and here is the way I use it in a component
user: User;
userSubscription: Subscription;
constructor(
public userService: UserService
) {
}
ngOnInit() {
this.userSubscription = this.userService.userSubject.subscribe(
(user: User) => {
this.user = user;
}
);
this.userService.getSingleUserFromServer();
this.userService.emitUser();
}
ngOnDestroy() {
this.userSubscription.unsubscribe();
}
Should I put this code in every component where I want to use the user or is there a way to definie globaly the user ?
You can use a BehaviourSubject which will hold the last value of whatever that service populates the userSubject with
public userSubject: BehaviourSubject<User> = new BehaviourSubject(null);
getSingleUserFromServer(): void {
//get your user from http
userSubject.next(result);
}
In you HTML you can use the async pipe to display the values of the inner array you want. Or just use it in your component by subscribing to the last emission of the behaviourSubject
//X.Component
public subscriptionKiller: Subject<void> = new Subject();
ngOnInit(): void {
this.userService.userSubject
.pipe(takeUntil(this.subscriptionKiller))
.subscribe((lastUser: User) => {
someMethod(this.userService.userSubject.value.arrayIWant);
}
}
ngOnDestroy(): void {
this.subscriptionKiller.next()
}

Getting 'undefined' after end of scope

I am using firebase auth and when a user logs it, I'm trying to get his uid (key) and save it.
This is my code (Here console.log(that.uid); returns the key):
loginWithEmail(email, password)
{
// Resolving scope problems in TypeScript
let that = this;
return this.af.auth.login(
{
email: email,
password: password
},
{
provider: AuthProviders.Password,
method: AuthMethods.Password,
}).then((user) =>
{
that.uid = user.uid;
// console.log(that.uid);
})
}
and I have a function that gets called from another component that returns the uid:
getUid()
{
console.log(this.uid);
return this.uid;
}
Here console.log(this.uid); returns 'undefined'.
Why does it happen and how can I resolve it?
EDIT:
I was read the answers below and understand that the problem is with async.. but I didn't find a solution how to save my data (in this example: user id) from async function to sync one.
I will be glad if someone can offer a solution to my code so I will be able to understand how to fix it and to use well async and sync.
EDIT2:
getUid() function is provide inside afservice whith the loginWithEmail(email, password) function.
getUid() was called from another component, in my case HomeComponent in this way:
export class HomeComponent implements OnInit
{
permission: number;
constructor(private afService: AF, private router: Router)
{
console.log(afService.getUid());
}
}
thanks.

Categories