I have a collection in Strapi called projects and I want to be able to fetch only the projects belonging to the currently logged in user. I'm using Next.js with NextAuth on the frontend and I'm currently filtering the results using:
/api/projects?filters[user][id][$eq]=${session.id}
This works fine except the endpoint still allows a user to fetch projects for all users if accessed directly. I'm thinking a better approach would be to setup a custom API endpoint in Strapi which would be something like /api/projects/:user. Is this the best way to acheive this? I've managed to setup a custom endpoint in Strapi using the CLI but I'm not sure what logic needs to go in the controller. Would modifiying an exisiting endpoint be better?
Any advice appreciated, thanks!
Custom endpoint create is good idea. I had same problem. Once i created custom endpoint then i got data with entitiyservice. It's work. Below image is my code.
./scr/api/[collection]/controllers/[collection].js
'use strict';
const { createCoreController } = require('#strapi/strapi').factories;
module.exports = createCoreController('api::user-profile.user-profile', ({ strapi }) => ({
async me(ctx) {
try {
const user = ctx.state.user;
const datas = await strapi.entityService.findMany("api::user-profile.user-profile", {
filters: {
user: {
id: user.id
}
}
})
return datas;
} catch (err) {
ctx.body = err;
}
}
}));
If you will use all endpoints in collection like (create,update,delete,find,findone). You must override the all endpoints . Example is the below.
'use strict';
const { createCoreController } = require('#strapi/strapi').factories;
module.exports = createCoreController('api::education.education', ({ strapi }) => ({
async create(ctx) {
try {
const user = ctx.state.user;
ctx.request.body.data.users_permissions_user = user.id
const datas = await strapi.entityService.create("api::education.education", {
...ctx.request.body
})
return datas;
} catch (err) {
ctx.body = err;
}
},
async update(ctx) {
try {
const user = ctx.state.user;
ctx.request.body.data.users_permissions_user = user.id
const { id } = ctx.params;
const experienceData = await strapi.entityService.findMany("api::education.education", {
filters: {
users_permissions_user: {
id: user.id
},
id: id
}
});
if (experienceData.length === 0) {
return {
data: null,
error: {
message: ''
}
}
}
const datas = await strapi.entityService.update("api::education.education", id, {
...ctx.request.body
})
return datas;
} catch (err) {
ctx.body = err;
}
},
async delete(ctx) {
try {
const user = ctx.state.user;
const { id } = ctx.params;
const experienceData = await strapi.entityService.findMany("api::education.education", {
filters: {
users_permissions_user: {
id: user.id
},
id: id
}
});
if (experienceData.length === 0) {
return {
data: null,
error: {
message: ''
}
}
}
const datas = await strapi.entityService.delete("api::education.education", id)
return datas;
} catch (err) {
ctx.body = err;
}
},
async findOne(ctx) {
try {
const user = ctx.state.user;
const { id } = ctx.params;
const experienceData = await strapi.entityService.findMany("api::education.education", {
filters: {
users_permissions_user: {
id: user.id
},
id: id
}
});
if (experienceData.length === 0) {
return {
data: null,
error: {
message: ''
}
}
}
const datas = await strapi.entityService.findOne("api::education.education", id)
return datas;
} catch (err) {
ctx.body = err;
}
},
async find(ctx) {
try {
const user = ctx.state.user;
const datas = await strapi.entityService.findMany("api::education.education", {
filters: {
users_permissions_user: {
id: user.id
}
}
})
return datas;
} catch (err) {
ctx.body = err;
}
},
}));
No extra endpoints and no extra codes.
Strapi v4
Yes, creating separate endpoint for this task would be great.
Instead of /api/projects/:user using this type of route, use /api/projects as you can get current logged in users details from ctx.state.user
No, Instead of modifying your existing controller create new controller and use that controller to satisfy your needs.
I ended up extending my controller. In src/api/controllers/project.js I made the following changes:
"use strict";
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::project.project", {
async find(ctx) {
const user = ctx.state.user;
ctx.query.filters = {
...(ctx.query.filters || {}),
user: user.id,
};
return super.find(ctx);
},
});
Then simply call the /api/projects endpoint.
Answer based on this guide Limit access of Strapi users to their own entries.
Related
I'm trying to pass in fetch which apparently isn't defined in my api libraries when using ssr:
<script context="module">
import setup from '$api/create-api';
import Jobs from '$api/jobs';
export async function load({ fetch }) {
setup(fetch);
const jobs = await Jobs.getAll();
return {
props: { jobs }
};
}
</script>
create-api.js
import { browser } from '$app/env';
let fetch = fetch || null;
async function api(path, body = {}, opts = {}) {
path = import.meta.env.VITE_API_ENDPOINT + path;
body = JSON.stringify(body);
const method = opts.method || 'GET';
const headers = {};
if (browser) {
const token = localStorage.getItem('token');
headers.Authorization = token ? 'Bearer ' + token : '';
}
const res = await fetch(path, {
method: opts.method || 'GET',
body: method === 'GET' ? null : body,
headers
});
if (res.ok) {
return await (opts.raw ? res.text() : res.json());
}
throw res;
}
export default api;
export const setup = (f) => {
fetch = f;
};
jobs.js
import api from './create-api';
class Jobs {
static async getAll() {
return await api('/jobs');
}
static async getAllMine() {
return await api('/jobs/me');
}
static async create(job) {
return await api('/jobs', job, { method: 'POST' });
}
static async update(job) {
return await api('/jobs/' + job.id, job, { method: 'PUT' });
}
static async deleteById(id) {
return await api('/jobs/' + id, {}, { method: 'DELETE' });
}
static async getById(id) {
console.log(id);
return await api('/jobs/' + id, {}, { method: 'GET' });
}
}
export default Jobs;
It seems you have to use the fetch passed into the script module for some reason. I tried installing node-fetch but got a ton of errors.
I'm currently creating an authentication feature in Nuxt and Firebase.
The login and logout process itself can be done and the header display changes accordingly, but there is an error in console when I press the login button.
Error content (in console)
Uncaught RangeError: Maximum call stack size exceeded
at Function.keys (<anonymous>)
code
Header.vue(This is the page containing the login button.)↓
googleLogin () {
const provider = new firebase.auth.GoogleAuthProvider()
auth.signInWithPopup(provider)
.then(res => {
this.dialogAuthVisible = false
this.$store.dispatch('auth/setUser',res.user)
}).catch(e => console.log(e))
}
store/auth.js↓
export const strict = false
export const state = () => ({
user: null
})
export const mutations = {
SET_USER (state, payload) {
state.user = payload
}
}
export const actions = {
setUser ({ commit }, user) {
commit('SET_USER',user)
}
}
export const getters = {
isAuthenticated (state) {
return !!state.user
}
}
default.vue↓
mounted () {
auth.onAuthStateChanged(user => {
const { uid, displayName, photoURL} = user
if (user) {
this.$store.dispatch('auth/setUser', { uid, displayName, photoURL})
} else {
this.$store.dispatch('auth/setUser', null)
}
})
}
If there's any information I'm missing, please let me know 🙇️.
Please teach me how to do this 🙇️.
I think the problem is in this code lines :
export const mutations = {
SET_USER (state, payload) {
state.user = payload
}
}
export const actions = {
setUser ({ commit }, user) {
commit('SET_USER',user)
}
}
There is a loop between this mutations and actions
Instead of setting the entire payload into the store object, I just picked the fields I needed, and that resolved the problem for me.
Before:
AUTH_STATUS_CHANGED ({commit}, data: any): any {
if (data && data.authUser) {
commit('SetAuthUser', data.authUser);
} else {
commit('SetAuthUser', null);
}
}
After:
AUTH_STATUS_CHANGED ({commit}, data: any): any {
if (data && data.authUser) {
const user = data.authUser;
commit('SetAuthUser', {
uid: user.uid,
email: user.email,
emailVerified: user.emailVerified,
displayName: user.displayName,
isAnonymous: user.isAnonymous,
photoURL: user.photoURL,
stsTokenManager: user.stsTokenManager,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
apiKey: user.apiKey,
});
} else {
commit('SetAuthUser', null);
}
}
Inside the mutation, just add the value received from the mutation payload.
I am setting a serverless application using AWS Amplify
My frontend app has the following code
import React, { Component } from 'react';
import './App.css';
import Layout from './Containers/Layout';
import { Amplify, API } from 'aws-amplify';
import aws_exports from './aws-exports';
Amplify.configure(aws_exports);
const apiName = 'top3DynamoDBAPI';
let path = '/listings/';
let partitionKey = 'Restaurant';
class App extends Component {
componentDidMount() {
API.get(apiName, path + partitionKey).then(response => {
console.log(response)
});
}
state = {
listings: {
}
}
render() {
return (
<div className="App">
<Layout />
</div>
);
}
}
export default App;
in my backend API the get method to retrieve items from the table is as follows
/********************************
* HTTP Get method for list objects *
********************************/
app.get(path + hashKeyPath, function(req, res) {
var condition = {}
condition[partitionKeyName] = {
ComparisonOperator: 'EQ'
}
if (userIdPresent && req.apiGateway) {
condition[partitionKeyName]['AttributeValueList'] = [req.apiGateway.event.requestContext.identity.cognitoIdentityId || UNAUTH ];
} else {
try {
condition[partitionKeyName]['AttributeValueList'] = [ convertUrlType(req.params[partitionKeyName], partitionKeyType) ];
} catch(err) {
res.statusCode = 500;
res.json({error: 'Wrong column type ' + err});
}
}
let queryParams = {
TableName: tableName,
KeyConditions: condition
}
dynamodb.query(queryParams, (err, data) => {
if (err) {
res.statusCode = 500;
res.json({error: 'Could not load items: ' + err});
} else {
res.json(data.Items);
}
});
});
In my Dynamo DB table, I have a primary partition which has categories and one of them is called 'Restaurant' . So in my App.js I set some variables and call the API to get the items in ComponentDidMount
const apiName = 'top3DynamoDBAPI';
let path = '/listings/';
let partitionKey = 'Restaurant';
componentDidMount() {
API.get(apiName, path + partitionKey).then(response => {
console.log(response)
});
this returns all the items from the table where the primary partition matches a value called 'Restaurant'
Now I have global Secondary Partition called 'Listing_Location'
which currently has two values -- Sydney and Brisbane.
The backend API uses DynamoDB's Document Client and has the following variable initialised
const userIdPresent = false; // TODO: update in case is required to use that definition
const partitionKeyName = "Listing_Category";
const partitionKeyType = "S";
const sortKeyName = "Listing_Id";
const sortKeyType = "S";
const hasSortKey = sortKeyName !== "";
const path = "/listings";
const UNAUTH = 'UNAUTH';
const hashKeyPath = '/:' + partitionKeyName;
const sortKeyPath = hasSortKey ? '/:' + sortKeyName : '';
I am stuck at trying to figure out how to pass the secondary partition to my backend so I can lookup items based on location. Please can you help with this.
I was able to solve it with a combination of info from
DynamoDb how to query a Global Secondary Index? and
https://medium.com/#ole.ersoy/sending-an-email-parameter-with-amplify-api-get-request-4c1c8dc0c952
Now, my App.js looks like
componentDidMount() {
let params = {
'queryStringParameters': {
location: 'Brisbane'
}
}
API.get(apiName, path, params).then(response => {
this.setState({
listings: response
})
console.log(response)
});
}
New get function is
/* NEW GET ATTEMPT*/
app.get(path, function (req, res) {
if (userIdPresent) {
req.body['userId'] = req.apiGateway.event.requestContext.identity.cognitoIdentityId || UNAUTH;
}
const location = req.query.location;
var queryItemParams = {
TableName: tableName,
IndexName: "ListingGSI",
KeyConditionExpression: "#location = :v_location",
ExpressionAttributeNames: {
"#location": "Listing_Location"
},
ExpressionAttributeValues: {
":v_location": location
}
};
dynamodb.query(queryItemParams, (err, data) => {
if (err) {
res.statusCode = 500;
res.json({ error: 'Could not load items: ' + err });
} else {
res.json(data.Items);
}
});
});
I want to call an action in created hook, wait until is done and in same hook to display the result. Is that possible?
I tried to put async / await in actions but doesn't help.
This is the action property with the async function in the store:
actions: {
async FETCH_USER({commit}) {
await firebase.firestore().collection('test').get().then(res => {
commit('FETCH_USER', res.docs[0].data())
})
}
}
created() {
this.FETCH_USER()
console.log(this.GET_USER)
},
methods: {
...mapActions([
'FETCH_USER'
]),
login() {
if(this.$refs.form.validate()) {
console.log('welcome')
}
}
},
computed: {
...mapGetters([
'GET_USER'
])
}
export default new Vuex.Store({
state: {
user: null
},
getters: {
GET_USER: state => state.user
},
mutations: {
FETCH_USER(state, user) {
state.user = user
}
},
actions: {
FETCH_USER({commit}) {
firebase.firestore().collection('test').get().then(res => {
commit('FETCH_USER', res.docs[0].data())
})
}
}
})
async/await version
async FETCH_USER({ commit }) {
const res = await firebase.firestore().collection('test').get()
const user = res.docs[0].data()
commit('FETCH_USER', user)
return user
}
async created() {
// The action returns the user out of convenience
const user = await this.FETCH_USER()
console.log(user)
// -- or --
// Access the user through the getter
await this.FETCH_USER()
console.log(this.GET_USER)
}
You need to await the action call because it is an async function.
Promise version
FETCH_USER({ commit }) {
return firebase.firestore().collection('test').get().then(res => {
const user = res.docs[0].data()
commit('FETCH_USER', user)
return user
})
}
created() {
this.FETCH_USER().then(user => {
console.log(user)
})
// -- or --
this.FETCH_USER().then(() => {
console.log(this.GET_USER)
})
}
I want to create a new user document in my Cloud Firestore database whenever a new user logs in. Each doc should have a unique id and I want a "uid" property for each user to match the unique auto-generated id for the doc. At first, I just always ran an update on the user, but I figured it could be helpful to separate my create and update logic. As you can see I haven't worked out how to query if a user exists, but I figured I should test the createUser function before continuing.
Anyway, while I was testing my createUser function I ran into a compilation error.
ERROR in src/app/services/auth.service.ts(64,22): error TS2554:
Expected 1 arguments, but got 0.
UPDATE:
When I try to run the function from localhost after compilation I get this error in the console.
Function CollectionReference.doc() requires its first argument to be
of type string, but it was: undefined
Here is my proposed solution:
import { Injectable } from '#angular/core';
import { User } from './../models/user.model';
import { PermissionsService } from './permissions.service';
import { auth } from 'firebase/app';
import { AngularFireAuth } from 'angularfire2/auth';
import {
AngularFirestore,
AngularFirestoreDocument,
AngularFirestoreCollection,
} from 'angularfire2/firestore';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
#Injectable({
providedIn: 'root',
})
export class AuthService {
usersCollection: AngularFirestoreCollection<User> = null;
user: Observable<User>;
constructor(
private afAuth: AngularFireAuth,
private db: AngularFirestore,
private permissionsService: PermissionsService,
) {
this.usersCollection = db.collection('users');
this.user = this.afAuth.authState.pipe(
switchMap((user) => {
if (user) {
return this.db
.doc<User>(`users/${user.uid}`)
.valueChanges();
} else {
return of(null);
}
}),
);
}
loginGoogle() {
const provider = new auth.GoogleAuthProvider();
return this.oAuthLogin(provider);
}
loginFacebook() {
const provider = new auth.FacebookAuthProvider();
return this.oAuthLogin(provider);
}
loginTwitter() {
const provider = new auth.TwitterAuthProvider();
return this.oAuthLogin(provider);
}
oAuthLogin(provider) {
return this.afAuth.auth.signInWithPopup(provider).then((credential) => {
//if(the user exists already)
//this.updateUserData(credential.user);
//else
this.createUser();
});
}
createUser() {
const newUserRef = this.usersCollection.doc<User>(); // Error here
let newUser: User;
this.user.subscribe((userData) => {
newUser = {
uid: newUserRef.id,
email: userData.email,
photoURL: userData.photoURL,
displayName: userData.displayName,
roles: {
member: true,
},
permissions: this.permissionsService.memberPermissions;
};
});
newUserRef
.set(newUser)
.then(() => {
console.log('created user');
})
.catch((err) => {
console.log('Error adding user: ' + err);
});
}
updateUserData(user) {
const userRef: AngularFirestoreDocument<any> = this.db.doc(
`users/${user.uid}`,
);
const userPermissions = this.addPermissions(userRef);
console.log(userPermissions); // This works
const data: User = {
uid: user.uid,
email: user.email,
photoURL: user.photoURL,
displayName: user.displayName,
roles: {
member: true,
}, // I need to make sure this keeps current user roles
permissions: userPermissions,
};
console.log(data); // This works
userRef
.set(data)
.then(() => {
console.log('Success: Data for userDoc overwritten');
})
.catch((err) => {
console.error('Error writing to userDoc: ' + err);
});
}
addPermissions(userRef) {
const tempPermissions = [];
userRef.valueChanges().subscribe((userdata) => {
if (userdata.roles.reader === true) {
tempPermissions.push(this.permissionsService.memberPermissions);
}
if (userdata.roles.author === true) {
tempPermissions.push(this.permissionsService.authorPermissions);
}
if (userdata.roles.admin === true) {
tempPermissions.push(this.permissionsService.adminPermissions);
}
});
return tempPermissions;
}
checkPermissions(permission: string) {
if (!this.user) {
return false;
} else {
this.user.subscribe((data) => {
for (const p of data.permissions) {
if (p === permission) {
return true;
}
}
return false;
});
}
}
logout() {
this.afAuth.auth.signOut();
this.user = null;
}
}
I checked the documentation on the .doc() function and it should work fine with 0 arguments. It should be returning an empty doc reference. However, it keeps throwing the error saying it expects 1 argument. Any idea why this isn't working?