Dev Server with vite + vue3, route 404 not found - javascript

Im in the process of moving an app from Vue 2 -> 3
I decided to take a moment to really upgrade and refactor all my repo and that led to using Vue3 recs on new tech, one being vite
My problem is I don't totally understand how the backend API process works so im struggling to move my api route from vue-cli to vite.
I would like to keep using the logic that call functions from api/users to remain in place but im open to a better option
Ultimately I get 404 - Not Found as my response which means its cant find the route
Heres my api/user.js
import request from '../utils/request'
export function login(data) {
return request({
url: '/user/login',
method: 'post',
data
})
}
export function getInfo(token) {
return request({
url: '/user/info',
method: 'get',
params: { token }
})
}
export function logout() {
return request({
url: '/user/logout',
method: 'post'
})
}
utils/Request.js
import axios from 'axios'
import { ElMessageBox, ElMessage } from 'element-plus'
import { userStore } from '../stores/user'
import { getToken } from '../utils/auth'
// create an axios instance
const service = axios.create({
baseURL: import.meta.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
const useStore = userStore;
console.log("Req", "Req Init");
if (useStore.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['X-Token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
console.log("failed") // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
console.log("Res", "Res Init");
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000) {
ElMessage({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
ElMessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
ElMessage({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
And a peek at my store that actually calls the endpoint
import { login, logout, getInfo } from '../api/user'
actions: { // user login
login({ commit }, userInfo) {
const { username, password } = userInfo
// **Call is made here to 'login'**
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
Lastly where the call originates from: this.store.login in my login.vue component
this.store.login('user/login', this.loginForm).then(() => {
this.$router.push({ path: this.redirect || '/' })
this.loading = false
}).catch(() => {
this.loading = false
})
This is my first StackOverflow post, so be kind if I need to include something else and thanks to any ideas or tips. Thank you
I've tried to search on different ports but the route still comes back as undefined.
I tried to change the vite config to include the server option but it still doesn't seem to help.
Not sure If I need to modify the config or not but I did have some settings related to the server mock on my old webpack config

Related

vue-router beforeEach function does not return item which is updated

I'm stuck with a problem. So here is the scenario. I put an axios request which takes the access token from cookies on store. Then I committed a mutation to make true isLoggedIn variable. Then I access this variable from Navbar to change menu items. It works. But when I try to access isLoggedIn variable with getters in beforeEach function, it turns still false. But it is true on Navbar.
user/actions.js which I request to backend to for authentication.
import axios from 'axios'
const checkUser = ({ commit }) => {
axios({
method: 'get',
url: 'http://localhost:3000/api/auth/VpW02cG0W2vGeGXs8DdLIq3dQ62qMd0',
withCredentials: true,
headers: {
Accept: "application/json",
},
})
.then((res) => {
commit('defineUser', res.data)
return true
})
.catch((err) => {
console.log(err)
return false
})
}
export default {
checkUser,
}
user/mutations.js which I set user and isLoggedIn variables
const defineUser = (state, res) => {
state.user = res.user
state.isLoggedIn = true
}
export default {
defineUser,
}
Then I call that action func in beforeEach in router
router.beforeEach(async (to, from, next) => {
const accessToken = VueCookies.get('access_token')
if (accessToken) { // first I check if there is an access token. I do that because I check every request if it is logged in. So I can manage Navbar.
await store.dispatch('user/checkUser')
if (store.getters['user/isLoggedIn']) { // HERE IS THE PROBLEM IT TURNS FALSE HERE. if there is an access token, then I check it with if mutation made isLoggedIn true and all doors are open for that user
next()
} else { // if it is false then show a toast and remove access token and reload the page
router.app.$bvToast.toast('You need to log in to see this page!', { // another question, when I deleted async it cannot read toast with only getter. If I put something else it works perfectly.
title: 'Unathorized',
variant: 'danger',
solid: true
})
VueCookies.remove('access_token')
router.go(router.currentRoute)
}
} else if (to.meta.requiresAuth) { // so if there is no access token and this page requires auth so show an toast
router.app.$bvToast.toast('You need to log in to see this page!', {
title: 'Unathorized',
variant: 'danger',
solid: true
})
} else { // if no requires auth and no access token then just get in the page
next()
}
})
If you need any other information please say, so I can share with you. Any help will be appreciated.
You are awaiting checkUser but it doesn't return a promise. Change it to:
const checkUser = ({ commit }) => {
return axios({ // notice the `return` statement
...
}
Alternatively, you could use async/await:
const checkUser = async ({ commit }) => { // async
await axios({ // await
...
}

How do I listen for new uploads from a specific channel in the YouTube API?

I am making a Discord bot, and I want it to be able to use the YouTube API to fetch new uploads from a specific channel.
I have searched elsewhere, but they all say how to upload videos, not how to track uploads.
Is this possible, and how can I do it?
Edit: Tried PubSubHubbub but it was very confusing and I couldn't get it to work
Here an example built on top of Node.js (v12) and Fastify and published with ngrok:
I wrote some comments explaining what it is happening:
const fastify = require('fastify')({ logger: true })
const xmlParser = require('fast-xml-parser')
const { URLSearchParams } = require('url')
const fetch = require('node-fetch')
// add an xml parser
fastify.addContentTypeParser('application/atom+xml', { parseAs: 'string' }, function (req, xmlString, done) {
try {
const body = xmlParser.parse(xmlString, {
attributeNamePrefix: '',
ignoreAttributes: false
})
done(null, body)
} catch (error) {
done(error)
}
})
// this endpoint needs for authentication
fastify.get('/', (request, reply) => {
reply.send(request.query['hub.challenge'])
})
// this endpoint will get the updates
fastify.post('/', (request, reply) => {
console.log(JSON.stringify(request.body, null, 2))
reply.code(204)
reply.send('ok')
})
fastify.listen(8080)
.then(() => {
// after the server has started, subscribe to the hub
// Parameter list: https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#rfc.section.5.1
const params = new URLSearchParams()
params.append('hub.callback', 'https://1f3dd0c63e78.ngrok.io') // you must have a public endpoint. get it with "ngrok http 8080"
params.append('hub.mode', 'subscribe')
params.append('hub.topic', 'https://www.youtube.com/xml/feeds/videos.xml?channel_id=UCfWbGF64qBSVM2Wq9fwrfrg')
params.append('hub.lease_seconds', '')
params.append('hub.secret', '')
params.append('hub.verify', 'sync')
params.append('hub.verify_token', '')
return fetch('https://pubsubhubbub.appspot.com/subscribe', {
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: params,
method: 'POST'
})
})
.then((res) => {
console.log(`The status must be 204. Received ${res.status}`)
// shows the error if something went wrong
if (res.status !== 204) {
return res.text().then(txt => console.log(txt))
}
})
I used my channel id to do some testing, consider that the notification is not in real-time, the POSTs are triggered after several minutes usually.

How to manage 429 errors in axios with react native?

I have a react native app that uses MongoDB as the database with express and node js I also use Axios to communicate with the client to the server
Now the app constantly sends and receives data from the database rapidly, e.g a user makes as much as 3 to 4 requests to and from the backend per second when the app is in use,
Everything works fine but there are a lot of 429 errors, how to handle this error or prevent it from occurring without compromising the users experiences a lot?
this below is the axios instanace
const instance = axios.create({ baseURL: 'http://9rv324283.ngrok.io' })
this below is fetching the data from the database
<NavigationEvents
onWillFocus={() => {
try {
const response = await instance.get('fetchNewDishes');
this.setState({data: response.data})
} catch(err) {
console.log(err)
}
}}>
this below is send data to the database
<TouchableOpacity onPress={() => instance.patch(`/postNewDish/${this.state.dish}`)}>
<Text style={{ fontSize: 16, color: '#555', padding: 15 }}>Post Dish</Text>
</TouchableOpacity>
I would suggest you to use axios interceptors to actually trace the error handling in axios , see below example :
import ax from 'axios';
import {config} from '../global/constant';
const baseUrl = config.apiUrl;
let axios = ax.create({
baseURL: baseUrl,
withCredentials: true,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
'Access-Control-Allow-Origin': '*',
},
});
axios.interceptors.request.use(req => handleRequest(req));
axios.interceptors.response.use(
res => handleResponse(res),
rej => handleError(rej),// here if its an error , then call handleError and do what you want to do with error.
);
// sending the error as promise.reject
const handleError = error => {
let errorResponse = {...error};
console.log({...error}, 'error');
return Promise.reject({
data: errorResponse.response.data,
code: errorResponse.response.status,
});
};
Hope it helps. feel free for doubts
Are you in control of the backend? It is possible there is a middleware that limits requests such as express-rate-limit
Make sure to either disable these middlewares, or allow many more requests per minute in the middleware configs.
I had a play around with this using https://httpstat.us/429/cors, which always returns error 429 with retry-after set to 5 (seconds), and came up with this using axios-retry:
import axios from "axios";
import axiosRetry from "axios-retry";
let instance = axios.create({ baseURL: "https://httpstat.us" });
axiosRetry(instance, {
retryCondition: (e) => {
return (
axiosRetry.isNetworkOrIdempotentRequestError(e) ||
e.response.status === 429
);
},
retryDelay: (retryCount, error) => {
if (error.response) {
const retry_after = error.response.headers["retry-after"];
if (retry_after) {
return retry_after;
}
}
// Can also just return 0 here for no delay if one isn't specified
return axiosRetry.exponentialDelay(retryCount);
}
});
// Test for error 429
instance({
url: "/429/cors",
method: "get"
})
.then((res) => {
console.log("429 res: ", res);
})
.catch((e) => {
console.log("429 e: ", e);
});
// Test to show that code isn't triggered by working API call
instance({
url: "/200/cors",
method: "get"
})
.then((res) => {
console.log("200 res: ", res);
})
.catch((e) => {
console.log("200 e: ", e);
});
I'm working on adding this to axios-retry properly for https://github.com/softonic/axios-retry/issues/72

Where to put API session auth token in SDK request methods?

I am using the ConnectyCube React Native SDK and have obtained an app auth token using their API. This token is required when making further requests - for example when logging in as a user. Their documentation says:
Upgrade session token (user login)
If you have an application session token, you can upgrade it to a user session by calling login method:
var userCredentials = {login: 'cubeuser', password: 'awesomepwd'};
ConnectyCube.login(userCredentials, function(error, user) {
});
The problem is it that when I use this method, I get an error in response saying 'Token is required'.
If I were interfacing with a REST API, I would put the token in the header of the request, but obviously in this instance I can't. So the question is, where do I put the token? I have it, the documentation just doesn't tell you how to use it! Any help appreciated.
Ok I came up with a fix. First of all I just tried passing the auth token in to the userCredntials object in the same way as in the documentation for social auth, that is absent from the description in my above code snippet taken from their docs.
Then I Promisified the API calls from within useEffect inside an async function to make sure everything was happening in the right order, and it works:
export default function App() {
const createAppSession = () => {
return new Promise((resolve, reject) => {
ConnectyCube.createSession((error, session) => {
!error
? resolve(session.token)
: reject(error, '=====1=====');
});
})
}
const loginUser = (credentials) => {
return new Promise((resolve, reject) => {
ConnectyCube.login(credentials, ((error, user) => {
!error
? resolve(user)
: reject(error, '=====2=====');
}));
})
}
useEffect(() => {
const ccFunc = async () => {
ConnectyCube.init(...config)
const appSessionToken = await createAppSession();
const userCredentials = { login: 'xxxxx', password: 'xxxxxxx', keys: { token: appSessionToken } };
const user = await loginUser(userCredentials);
console.log(user);
}
ccFunc()
}, []);
Hope it works....
please implement it by yourself...just take an understanding from code below.
code says: send the username and password to api...if all ok then authenticate else throw error ...if all ok..then store the returned token is asyncStorage...you can create the storage by any name you like...and use the token eveywhere in your app.
SignInUser = async () => {
this.setState({
username: this.state.username,
password:this.state.password,
})
if(this.state.username && this.state.password !== null){
try{
this.setState({
loading:true
})
const response = await fetch('YOUR API', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: this.state.username,
password: this.state.password
})
});
var promiseResponse = await response.json()
console.log(promiseResponse.token);
try {
await AsyncStorage.setItem('STORE_YOUR_LOGIN_TOKEN_HERE', JSON.stringify(promiseResponse.token));
console.log('Token Stored In Async Storage');
let tokenFromAsync = await AsyncStorage.getItem('STORE_YOUR_LOGIN_TOKEN_HERE');
console.log('Getting Token From Async...')
tokenFromAsync = JSON.parse(tokenFromAsync)
if(tokenFromAsync !== null){
console.log(tokenFromAsync);
this.setState({
loading:false
})
this.props.navigation.navigate('Tabnav');
}
} catch (error) {
// saving error
console.log(`ERROR OCCURED ${error}`)
}
//this.props.navigation.navigate('Tabnav')
} catch(error){
console.log(`COULDN'T SIGN IN ${error}`)
}
} else {
this.setState({
msg:'Invalid Credentials',
label:'red'
});
}
}
This is how i got the login to work in their sample react native app 1. i created a credentials object like this in my custom login function in src>components>AuthScreen>AuthForm.js
var credentials = {id:'',login: this.state.login,password: this.state.password}
2.I used their _signIn(credentials) function and set the 'id' attribute of my credentials object after their UserService.signin(credentials) resolved with a user object. (the resolved user object contained the logged-in user's id i.e user.id). Then it worked. This is how the code looked for the signin after the little tweak.
loginUser() { //my custom signin function
var credentials = {id:'',login: this.state.login,password: this.state.password} //my credentials object
this._signIn(credentials)
}
_signIn(userCredentials) { //their signin function
this.props.userIsLogging(true);
UserService.signin(userCredentials)
.then((user) => {
userCredentials.id = user.id //setting id of my credentials object after promise resolved
ChatService.connect(userCredentials) //using my credentials object with id value set
.then((contacts) => {
console.warn(contacts)
this.props.userLogin(user);
this.props.userIsLogging(false);
Actions.videochat(); //login worked
})
.catch(e => {
this.props.userIsLogging(false);
alert(`Error.\n\n${JSON.stringify(e)}`);
})
})
.catch(e => {
this.props.userIsLogging(false);
alert(`Error.\n\n${JSON.stringify(e)}`);
})
}

How can you use axios interceptors?

I have seen axios documentation, but all it says is
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
Also many tutorials only show this code but I am confused what it is used for, can someone please give me simple example to follow.
To talk in simple terms, it is more of a checkpoint for every HTTP action. Every API call that has been made, is passed through this interceptor.
So, why two interceptors?
An API call is made up of two halves, a request, and a response. Since it behaves like a checkpoint, the request and the response have separate interceptors.
Some request interceptor use cases -
Assume you want to check before making a request if your credentials are valid. So, instead of actually making an API call, you can check at the interceptor level that your credentials are valid.
Assume you need to attach a token to every request made, instead of duplicating the token addition logic at every Axios call, you can make an interceptor that attaches a token on every request that is made.
Some response interceptor use cases -
Assume you got a response, and judging by the API responses you want to deduce that the user is logged in. So, in the response interceptor, you can initialize a class that handles the user logged in state and update it accordingly on the response object you received.
Assume you have requested some API with valid API credentials, but you do not have the valid role to access the data. So, you can trigger an alert from the response interceptor saying that the user is not allowed. This way you'll be saved from the unauthorized API error handling that you would have to perform on every Axios request that you made.
Here are some code examples
The request interceptor
One can print the configuration object of axios (if need be) by doing (in this case, by checking the environment variable):
const DEBUG = process.env.NODE_ENV === "development";
axios.interceptors.request.use((config) => {
/** In dev, intercepts request and logs it into console for dev */
if (DEBUG) { console.info("✉️ ", config); }
return config;
}, (error) => {
if (DEBUG) { console.error("✉️ ", error); }
return Promise.reject(error);
});
If one wants to check what headers are being passed/add any more generic headers, it is available in the config.headers object. For example:
axios.interceptors.request.use((config) => {
config.headers.genericKey = "someGenericValue";
return config;
}, (error) => {
return Promise.reject(error);
});
In case it's a GET request, the query parameters being sent can be found in config.params object.
The response interceptor
You can even optionally parse the API response at the interceptor level and pass the parsed response down instead of the original response. It might save you the time of writing the parsing logic again and again in case the API is used in the same way in multiple places. One way to do that is by passing an extra parameter in the api-request and use the same parameter in the response interceptor to perform your action. For example:
//Assume we pass an extra parameter "parse: true"
axios.get("/city-list", { parse: true });
Once, in the response interceptor, we can use it like:
axios.interceptors.response.use((response) => {
if (response.config.parse) {
//perform the manipulation here and change the response object
}
return response;
}, (error) => {
return Promise.reject(error.message);
});
So, in this case, whenever there is a parse object in response.config, the manipulation is done, for the rest of the cases, it'll work as-is.
You can even view the arriving HTTP codes and then make the decision. For example:
axios.interceptors.response.use((response) => {
if(response.status === 401) {
alert("You are not authorized");
}
return response;
}, (error) => {
if (error.response && error.response.data) {
return Promise.reject(error.response.data);
}
return Promise.reject(error.message);
});
You can use this code for example, if you want to catch the time that takes from the moment that the request was sent until the moment you received the response:
const axios = require("axios");
(async () => {
axios.interceptors.request.use(
function (req) {
req.time = { startTime: new Date() };
return req;
},
(err) => {
return Promise.reject(err);
}
);
axios.interceptors.response.use(
function (res) {
res.config.time.endTime = new Date();
res.duration =
res.config.time.endTime - res.config.time.startTime;
return res;
},
(err) => {
return Promise.reject(err);
}
);
axios
.get("http://localhost:3000")
.then((res) => {
console.log(res.duration)
})
.catch((err) => {
console.log(err);
});
})();
It is like a middle-ware, basically it is added on any request (be it GET, POST, PUT, DELETE) or on any response (the response you get from the server).
It is often used for cases where authorisation is involved.
Have a look at this: Axios interceptors and asynchronous login
Here is another article about this, with a different example: https://medium.com/#danielalvidrez/handling-error-responses-with-grace-b6fd3c5886f0
So the gist of one of the examples is that you could use interceptor to detect if your authorisation token is expired ( if you get 403 for example ) and to redirect the page.
I will give you more practical use-case which I used in my real world projects. I usually use, request interceptor for token related staff (accessToken, refreshToken), e.g., whether token is not expired, if so, then update it with refreshToken and hold all other calls until it resolves. But what I like most is axios response interceptors where you can put your apps global error handling logic like below:
httpClient.interceptors.response.use(
(response: AxiosResponse) => {
// Any status code that lie within the range of 2xx cause this function to trigger
return response.data;
},
(err: AxiosError) => {
// Any status codes that falls outside the range of 2xx cause this function to trigger
const status = err.response?.status || 500;
// we can handle global errors here
switch (status) {
// authentication (token related issues)
case 401: {
return Promise.reject(new APIError(err.message, 409));
}
// forbidden (permission related issues)
case 403: {
return Promise.reject(new APIError(err.message, 409));
}
// bad request
case 400: {
return Promise.reject(new APIError(err.message, 400));
}
// not found
case 404: {
return Promise.reject(new APIError(err.message, 404));
}
// conflict
case 409: {
return Promise.reject(new APIError(err.message, 409));
}
// unprocessable
case 422: {
return Promise.reject(new APIError(err.message, 422));
}
// generic api error (server related) unexpected
default: {
return Promise.reject(new APIError(err.message, 500));
}
}
}
);
How about this. You create a new Axios instance and attach an interceptor to it. Then you can use that interceptor anywhere in your app
export const axiosAuth = axios.create()
//we intercept every requests
axiosAuth.interceptors.request.use(async function(config){
//anything you want to attach to the requests such as token
return config;
}, error => {
return Promise.reject(error)
})
//we intercept every response
axiosAuth.interceptors.request.use(async function(config){
return config;
}, error => {
//check for authentication or anything like that
return Promise.reject(error)
})
Then you use axiosAuth the same way you use axios
This is the way I used to do in my project. The code snippet refers how to use access and refresh token in the axios interceptors and will help to implements refresh token functionalities.
const API_URL =
process.env.NODE_ENV === 'development'
? 'http://localhost:8080/admin/api'
: '/admin-app/admin/api';
const Service = axios.create({
baseURL: API_URL,
headers: {
Accept: 'application/json',
},
});
Service.interceptors.request.use(
config => {
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
config.headers.common = { Authorization: `Bearer ${accessToken}` };
}
return config;
},
error => {
Promise.reject(error.response || error.message);
}
);
Service.interceptors.response.use(
response => {
return response;
},
error => {
let originalRequest = error.config;
let refreshToken = localStorage.getItem('refreshToken');
const username = EmailDecoder(); // decode email from jwt token subject
if (
refreshToken &&
error.response.status === 403 &&
!originalRequest._retry &&
username
) {
originalRequest._retry = true;
return axios
.post(`${API_URL}/authentication/refresh`, {
refreshToken: refreshToken,
username,
})
.then(res => {
if (res.status === 200) {
localStorage.setItem(
'accessToken',
res.data.accessToken
);
localStorage.setItem(
'refreshToken',
res.data.refreshToken
);
originalRequest.headers[
'Authorization'
] = `Bearer ${res.data.accessToken}`;
return axios(originalRequest);
}
})
.catch(() => {
localStorage.clear();
location.reload();
});
}
return Promise.reject(error.response || error.message);
}
);
export default Service;
I have implemented in the following way
httpConfig.js
import axios from 'axios'
import { baseURL } from '../utils/config'
import { SetupInterceptors } from './SetupInterceptors'
const http = axios.create({
baseURL: baseURL
})
SetupInterceptors(http)
export default http
SetupInterceptors.js
import { baseURL } from '../utils/config'
export const SetupInterceptors = http => {
http.interceptors.request.use(
config => {
config.headers['token'] = `${localStorage.getItem('token')}`
config.headers['content-type'] = 'application/json'
return config
},
error => {
return Promise.reject(error)
}
)
http.interceptors.response.use(function(response) {
return response
}, function (error) {
const status = error?.response?.status || 0
const resBaseURL = error?.response?.config?.baseURL
if (resBaseURL === baseURL && status === 401) {
if (localStorage.getItem('token')) {
localStorage.clear()
window.location.assign('/')
return Promise.reject(error)
} else {
return Promise.reject(error)
}
}
return Promise.reject(error)
})
}
export default SetupInterceptors
Reference : link

Categories

Resources