implement profile photo upload

This commit is contained in:
Nuwan 2024-08-29 17:08:19 +05:30
parent 9668b59e23
commit 3175f77b7f
14 changed files with 279 additions and 137 deletions

View File

@ -4,59 +4,65 @@ import { DropdownItem, DropdownMenu, DropdownToggle, Dropdown } from 'reactstrap
import { useAuth } from '../../context/UserAuth';
import JKProfileAvatar from '../profile/JKProfileAvatar';
import { useCookies } from 'react-cookie';
import { useHistory } from "react-router-dom";
import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
// import useUserProfile from '../../hooks/useUserProfile';
import { useAppData } from '../../context/AppDataContext';
const ProfileDropdown = () => {
const { t } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false);
const toggle = () => setDropdownOpen(prevState => !prevState);
const { isAuthenticated, currentUser, setCurrentUser, logout } = useAuth();
const { isAuthenticated, currentUser, setCurrentUser, logout, currentUserProfile } = useAuth();
const [cookies, setCookie, removeCookie] = useCookies(['remember_token']);
const history = useHistory();
const handleLogout = async (event) => {
// const { photoUrl } = useUserProfile(currentUser);
const { appData } = useAppData();
const { currentUserPhotoUrl } = appData;
const handleLogout = async event => {
event.preventDefault();
removeCookie('remember_token', {
domain: `.${process.env.REACT_APP_ORIGIN}`
});
setCurrentUser(null);
await logout()
history.push('/')
await logout();
history.push('/');
};
return (
<>
{isAuthenticated &&
<Dropdown
nav
inNavbar
data-testid="navbarTopProfileDropdown"
isOpen={dropdownOpen}
toggle={toggle}
// onMouseOver={() => {
// let windowWidth = window.innerWidth;
// windowWidth > 992 && setDropdownOpen(true);
// }}
// onMouseLeave={() => {
// let windowWidth = window.innerWidth;
// windowWidth > 992 && setDropdownOpen(false);
// }}
>
<DropdownToggle nav className="pr-0">
<JKProfileAvatar src={currentUser.photo_url} className="d-block d-lg-none d-xl-none" />
{/* <span className="d-none d-lg-block">{currentUser && currentUser.name}</span> */}
</DropdownToggle>
<DropdownMenu right className="dropdown-menu-card">
<div className="bg-white rounded-soft py-2">
{/* <DropdownItem tag={Link} to="/pages/settings">
{isAuthenticated && (
<Dropdown
nav
inNavbar
data-testid="navbarTopProfileDropdown"
isOpen={dropdownOpen}
toggle={toggle}
// onMouseOver={() => {
// let windowWidth = window.innerWidth;
// windowWidth > 992 && setDropdownOpen(true);
// }}
// onMouseLeave={() => {
// let windowWidth = window.innerWidth;
// windowWidth > 992 && setDropdownOpen(false);
// }}
>
<DropdownToggle nav className="pr-0">
{<JKProfileAvatar src={currentUserPhotoUrl} className="d-none d-lg-block d-xl-block" />}
{/* <span className="d-none d-lg-block">{currentUser && currentUser.name}</span> */}
</DropdownToggle>
<DropdownMenu right className="dropdown-menu-card">
<div className="bg-white rounded-soft py-2">
{/* <DropdownItem tag={Link} to="/pages/settings">
My Profile
</DropdownItem> */}
<DropdownItem onClick={handleLogout}>{t('signout', { ns: 'auth' })}</DropdownItem>
</div>
</DropdownMenu>
</Dropdown>
}
<DropdownItem onClick={handleLogout}>{t('signout', { ns: 'auth' })}</DropdownItem>
</div>
</DropdownMenu>
</Dropdown>
)}
</>
);
};

View File

@ -1,5 +1,5 @@
import React, { useRef, useEffect, useLayoutEffect, useState, useReducer } from 'react';
import { Card, CardBody, Col, Row, CardHeader, Form, FormGroup, Label, Input, Button } from 'reactstrap';
import { Card, CardBody, Col, Row, CardHeader, Form, FormGroup, Label, Input } from 'reactstrap';
import Select from 'react-select';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
@ -7,7 +7,6 @@ import JKProfileAvatar from '../profile/JKProfileAvatar';
import { useAuth } from '../../context/UserAuth';
import { useForm, Controller } from 'react-hook-form';
import {
getPersonById,
getInstruments,
getGenres,
updateUser,
@ -18,6 +17,8 @@ import {
import JKProfileAvatarUpload from '../profile/JKProfileAvatarUpload';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Prompt } from 'react-router';
// import useUserProfile from '../../hooks/useUserProfile';
import { useAppData } from '../../context/AppDataContext';
function JKEditProfile() {
const { t } = useTranslation('profile');
@ -25,7 +26,6 @@ function JKEditProfile() {
const [musicInstruments, setMusicInstruments] = useState([]);
const [genres, setGenres] = useState([]);
const [instrumentsInitialLoadingDone, setInstrumentsInitialLoadingDone] = useState(false);
const [currentUserLoaded, setCurrentUserLoaded] = useState(false);
const [genreInitialLoadingDone, setGenreInitialLoadingDone] = useState(false);
const [countries, setCountries] = useState([]);
const [regions, setRegions] = useState([]);
@ -33,6 +33,10 @@ function JKEditProfile() {
const [showAvatarUpload, setShowAvatarUpload] = useState(false);
const [updating, setUpdating] = useState(false);
// const { userProfile, photoUrl } = useUserProfile(currentUser);
const { appData } = useAppData();
const { currentUserPhotoUrl, userProfile } = appData;
const [_, forceUpdate] = useReducer(x => x + 1, 0);
const saveTimeoutRef = useRef(null);
@ -62,35 +66,18 @@ function JKEditProfile() {
}
});
useLayoutEffect(() => {
if (currentUser && !currentUserLoaded) {
setCurrentUserLoaded(true);
fetchCurentUser().then(data => {
console.log('userData', data);
updateUserData(data);
fetchInstruments();
fetchGenres();
fetchCountries();
});
}
}, [currentUser]);
useEffect(() => {
if (!userProfile) return;
updateFormData(userProfile);
}, [userProfile]);
const fetchCurentUser = () => {
return new Promise((resolve, reject) => {
getPersonById(currentUser.id)
.then(response => {
if (response.ok) {
return response.json();
} else {
reject('Error fetching user data');
}
})
.then(data => resolve(data))
.catch(error => reject(error));
});
};
useEffect(() => {
fetchInstruments();
fetchGenres();
fetchCountries();
}, []);
const updateUserData = data => {
const updateFormData = data => {
setValue('firstName', data.first_name);
setValue('lastName', data.last_name);
setValue('country', data.country ? data.country : '');
@ -221,7 +208,6 @@ function JKEditProfile() {
const onSubmit = data => console.log(data);
const handleInstrumentSelect = (e, musicInstrument) => {
//alert(e.target.checked)
if (e.target.checked) {
const userInstruments = getValues('instruments');
const thisInstrument = userInstruments.find(
@ -481,7 +467,7 @@ function JKEditProfile() {
>
<div className="d-flex align-items-center">
<div>
<JKProfileAvatar src={currentUser.photo_url} size="3xl" />
{ <JKProfileAvatar src={currentUserPhotoUrl} size="3xl" /> }
</div>
<div>
<FontAwesomeIcon icon={['fas', 'edit']} className="ml-2 mr-1" />

View File

@ -5,30 +5,19 @@ import { useTranslation } from 'react-i18next';
import JKModalDialog from '../common/JKModalDialog';
import JKProfileAvatar from './JKProfileAvatar';
import { useAuth } from '../../context/UserAuth';
import { getUserDetail, updateAvatar } from '../../helpers/rest';
import { deleteAvatar, updateAvatar } from '../../helpers/rest';
import { toast } from 'react-toastify';
//import useUserProfile from '../../hooks/useUserProfile';
import { useAppData } from '../../context/AppDataContext';
import { set } from 'react-hook-form';
const JKProfileAvatarUpload = ({ show, toggle }) => {
const { t } = useTranslation('profile');
const { currentUser } = useAuth();
const [userDetails, setUserDetails] = useState(null);
useEffect(() => {
if (currentUser) {
console.log('DEBUG', currentUser.id);
getUserDetail({ id: currentUser.id })
.then(response => {
if (response.ok) {
return response.json();
}
})
.then(data => {
setUserDetails(data);
})
.catch(error => {
console.error(error);
});
}
}, [currentUser]);
// const { photoUrl } = useUserProfile(currentUser);
const { appData, setAppData } = useAppData();
const { currentUserPhotoUrl } = appData;
const [isProcessing, setIsProsessing] = useState(false);
const openFilePicker = () => {
const client = filestack.init(window.gon.fp_apikey);
@ -45,7 +34,13 @@ const JKProfileAvatarUpload = ({ show, toggle }) => {
},
storeTo: {
location: 's3',
path: `${window.gon.fp_upload_dir}/users/${currentUser.id}/`
path: `${window.gon.fp_upload_dir}/${currentUser.id}/`
},
onFileUploadStarted: () => {
setIsProsessing(true);
},
onFileUploadFailed: () => {
setIsProsessing(false);
},
onUploadDone: res => {
console.log('onUploadDone', res);
@ -54,46 +49,70 @@ const JKProfileAvatarUpload = ({ show, toggle }) => {
if (res.filesUploaded.length > 0) {
const opts = {
id: currentUser.id,
original_fpfile: userDetails.original_fpfile ? userDetails.original_fpfile : null,
cropped_fpfile: res.filesUploaded[0].url,
cropped_large_fpfile: null,
cropped_selection: null,
}
if(res.filesUploaded[0].cropped){
opts['crop_selection'] = {
x: res.filesUploaded[0].cropped.cropArea.position[0],
y: res.filesUploaded[0].cropped.cropArea.position[1],
w: res.filesUploaded[0].cropped.cropArea.size[0],
h: res.filesUploaded[0].cropped.cropArea.size[1],
};
url: res.filesUploaded[0].url,
// cropped_selection: null,
}
// if(res.filesUploaded[0].cropped){
// opts['crop_selection'] = {
// x: res.filesUploaded[0].cropped.cropArea.position[0],
// y: res.filesUploaded[0].cropped.cropArea.position[1],
// w: res.filesUploaded[0].cropped.cropArea.size[0],
// h: res.filesUploaded[0].cropped.cropArea.size[1],
// };
// }
updateAvatar(opts).then(response => {
if (response.ok) {
return response.json();
}
}).then(data => {
alert('Photo updated successfully');
console.log('DEBUG', data);
console.log('photo upload success', data);
setAppData({
...appData,
currentUserPhotoUrl: data.v2_photo_url
});
toast.success('Success! Your avatar has been updated.');
setIsProsessing(false);
}).catch(error => {
console.error(error);
toast.error('An error encountered when updating avatar.');
setIsProsessing(false);
});
}
}
};
client.picker(options).open();
};
const deleteProfileAvatar = () => {
setIsProsessing(true);
deleteAvatar(currentUser.id).then(response => {
if (response.ok) {
setAppData({
...appData,
currentUserPhotoUrl: null
});
toast.success('Success! Your avatar has been deleted.');
}
}).catch(error => {
console.error(error);
toast.error('An error encountered when deleting avatar.');
}).finally(() => {
setIsProsessing(false);
});
}
return (
<JKModalDialog show={show} onToggle={toggle} title={t('photo_modal.title', { ns: 'profile' })} showFooter={true}>
<div className="d-flex flex-column">
<div className="d-flex justify-content-center">
<JKProfileAvatar src={currentUser.photo_url} size="5xl" />
{ <JKProfileAvatar src={currentUserPhotoUrl} size="5xl" /> }
</div>
<div className="d-flex justify-content-center mt-2">
<Button color="primary" className="ml-2" onClick={openFilePicker}>
<Button color="primary" className="ml-2" onClick={openFilePicker} disabled={isProcessing}>
{t('photo_modal.upload', { ns: 'profile' })}
</Button>
<Button color="secondary" outline className="ml-2" onClick={() => {}}>
<Button color="secondary" outline className="ml-2" onClick={deleteProfileAvatar} disabled={isProcessing}>
{t('photo_modal.delete', { ns: 'profile' })}
</Button>
</div>

View File

@ -0,0 +1,21 @@
import React from 'react';
import useUserProfile from '../hooks/useUserProfile';
import { useAuth } from './UserAuth';
// AppDataContext.js
// this context is used to store app data that is shared across the app
const AppDataContext = React.createContext(null);
export const AppDataProvider = ({ children }) => {
const [appData, setAppData] = React.useState({});
const { currentUser } = useAuth();
const { userProfile, photoUrl } = useUserProfile(currentUser);
React.useEffect(() => {
setAppData({ userProfile, currentUserPhotoUrl: photoUrl });
}, [currentUser, userProfile, photoUrl]);
return <AppDataContext.Provider value={{ appData, setAppData }}>{children}</AppDataContext.Provider>;
};
export const useAppData = () => React.useContext(AppDataContext);

View File

@ -1,32 +1,31 @@
import React, { useState, useEffect, createContext, useContext } from 'react';
import PropTypes from 'prop-types';
import { checkIsAuthenticated, authSignUp, authLogin, authLogout } from '../services/auth'
import { checkIsAuthenticated, authSignUp, authLogin, authLogout } from '../services/auth';
export const UserAuthContext = createContext({});
export const useAuth = () => useContext(UserAuthContext)
export const useAuth = () => useContext(UserAuthContext);
export default function UserAuth({ children, path }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [currentUser, setCurrentUser] = useState(null)
const [currentUser, setCurrentUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
//console.log('checking auth for', path);
checkAuth();
}, [path]);
const checkAuth = () =>
checkIsAuthenticated()
.then(resp => resp.json())
.then((user) => {
.then(user => {
window.currentUser = user;
setCurrentUser(user)
setIsAuthenticated(true)
setCurrentUser(user);
setIsAuthenticated(true);
})
.catch(() => {
setIsAuthenticated(false)
setCurrentUser(null)
setIsAuthenticated(false);
setCurrentUser(null);
window.currentUser = null;
})
.then(() => setIsLoading(false));
@ -40,7 +39,7 @@ export default function UserAuth({ children, path }) {
});
const logout = () => {
authLogout()
authLogout();
setIsAuthenticated(false);
};
@ -53,7 +52,9 @@ export default function UserAuth({ children, path }) {
});
return (
<UserAuthContext.Provider value={{ currentUser, setCurrentUser, isAuthenticated, isLoading, login, logout, signUp }}>
<UserAuthContext.Provider
value={{ currentUser, setCurrentUser, isAuthenticated, isLoading, login, logout, signUp }}
>
{children}
</UserAuthContext.Provider>
);

View File

@ -546,17 +546,25 @@ export const deleteMixdown = id => {
export const updateAvatar = options => {
const { id, ...rest } = options;
const opts = {
original_fpfile: rest['original_fpfile'],
cropped_fpfile: rest['cropped_fpfile'],
cropped_large_fpfile: rest['cropped_large_fpfile'],
crop_selection: rest['crop_selection']
url: rest['url'],
// crop_selection: rest['crop_selection']
};
return new Promise((resolve, reject) => {
apiFetch(`/users/${id}/avatar`, {
apiFetch(`/users/${id}/avatar_v2`, {
method: 'POST',
body: JSON.stringify(opts)
})
.then(response => resolve(response))
.catch(error => reject(error));
});
}
export const deleteAvatar = id => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${id}/avatar_v2`, {
method: 'DELETE'
})
.then(response => resolve(response))
.catch(error => reject(error));
});
}

View File

@ -0,0 +1,44 @@
import { getPersonById } from '../helpers/rest';
import { useEffect, useState, useMemo } from 'react';
const useUserProfile = (user) => {
const [userProfile, setUserProfile] = useState(null)
useEffect(() => {
if (!user) {
setUserProfile(null);
return;
}
getPersonById(user.id)
.then(response => {
if (response.ok) {
return response.json();
}
})
.then(data => {
setUserProfile(data)
})
.catch(error => console.error(error));
return () => {
setUserProfile(null);
}
}, [user]);
const photoUrl = useMemo(() => {
if(userProfile && userProfile.v2_photo_uploaded){
return userProfile.v2_photo_url
}else if(userProfile && !userProfile.v2_photo_uploaded){
return user.photo_url
}
return null
}, [userProfile])
return{
userProfile,
photoUrl
}
}
export default useUserProfile;

View File

@ -4,12 +4,10 @@ import PropTypes from 'prop-types';
import DashboardMain from '../components/dashboard/JKDashboardMain';
import UserAuth from '../context/UserAuth';
import { BrowserQueryProvider } from '../context/BrowserQuery';
import { NativeAppProvider } from '../context/NativeAppContext';
import { JKLobbyChatProvider } from '../components/sessions/JKLobbyChatContext';
import { AppRoutesProvider } from '../context/AppRoutesContext';
import { AppDataProvider } from '../context/AppDataContext';
const DashboardLayout = ({ location }) => {
useEffect(() => {
@ -19,13 +17,15 @@ const DashboardLayout = ({ location }) => {
return (
<UserAuth path={location.pathname}>
<AppRoutesProvider>
<BrowserQueryProvider>
<NativeAppProvider>
<JKLobbyChatProvider>
<DashboardMain />
</JKLobbyChatProvider>
</NativeAppProvider>
</BrowserQueryProvider>
<AppDataProvider>
<BrowserQueryProvider>
<NativeAppProvider>
<JKLobbyChatProvider>
<DashboardMain />
</JKLobbyChatProvider>
</NativeAppProvider>
</BrowserQueryProvider>
</AppDataProvider>
</AppRoutesProvider>
</UserAuth>
);

View File

@ -0,0 +1,8 @@
class AddV2PhotoAttributes < ActiveRecord::Migration
def self.up
execute("ALTER TABLE public.users ADD COLUMN v2_photo_url VARCHAR(2048); ALTER TABLE public.users ADD COLUMN v2_photo_uploaded BOOLEAN; UPDATE public.users SET v2_photo_uploaded = FALSE; ALTER TABLE public.users ALTER COLUMN v2_photo_uploaded SET DEFAULT FALSE;")
end
def self.down
execute("ALTER TABLE public.users DROP COLUMN v2_photo_url; ALTER TABLE public.users DROP COLUMN v2_photo_uploaded;")
end
end

View File

@ -73,7 +73,8 @@ module JamRuby
after_save :update_teacher_pct
attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection, :used_current_month, :used_month_play_time
attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection, :used_current_month, :used_month_play_time,
:v2_photo_url, :v2_photo_uploaded
# updating_password corresponds to a lost_password
attr_accessor :test_drive_packaging, :validate_instruments, :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card, :purchase_required, :user_type
@ -295,7 +296,7 @@ module JamRuby
validate :validate_musician_instruments, if: :validate_instruments
validate :validate_current_password
validate :validate_update_email
validate :validate_avatar_info
validate :validate_avatar_info, unless: :avatar_v2_available?
validate :email_case_insensitive_uniqueness
validate :validate_spammy_names
validate :update_email_case_insensitive_uniqueness, :if => :updating_email
@ -671,6 +672,10 @@ module JamRuby
end
end
def avatar_v2_available?
self.v2_photo_url.present?
end
def email_case_insensitive_uniqueness
# using the case insensitive unique check of active record will downcase the field, which is not what we want--we want to preserve original casing
search = User.where("email ILIKE ?", self.email).first
@ -2054,6 +2059,13 @@ module JamRuby
)
end
def update_avatar_v2(url)
self.update_attributes(
:v2_photo_url => url,
:v2_photo_uploaded => true
)
end
def delete_avatar(aws_bucket)
User.transaction do
@ -2078,6 +2090,19 @@ module JamRuby
end
def delete_avatar_v2(aws_bucket)
User.transaction do
#TODO: delete the v2_photo_url from s3
# unless self.v2_photo_url.nil?
# S3Util.delete(aws_bucket, self.v2_photo_url)
# end
return self.update_attributes(
:v2_photo_url => nil
)
end
end
# throws RecordNotFound if signup token is invalid; i.e., if it's nil, empty string, or not belonging to a user
def self.signup_confirm(signup_token)
if signup_token.nil? || signup_token.empty?

View File

@ -12,7 +12,7 @@ class ApiUsersController < ApiController
:friend_show, :friend_destroy, # friends
:notification_index, :notification_destroy, # notifications
:band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations
:set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy, :request_reset_password,
:set_password, :begin_update_email, :update_avatar, :update_avatar_v2, :delete_avatar, :delete_avatar_v2, :generate_filepicker_policy, :request_reset_password,
:share_session, :share_recording,
:affiliate_report, :audio_latency, :get_latencies, :broadcast_notification, :redeem_giftcard, :post_app_interactions]
@ -626,8 +626,6 @@ class ApiUsersController < ApiController
cropped_large_fpfile = params[:cropped_large_fpfile]
crop_selection = params[:crop_selection]
debugger
# public bucket to allow images to be available to public
@user.update_avatar(original_fpfile, cropped_fpfile, cropped_large_fpfile, crop_selection, Rails.application.config.aws_bucket_public)
@ -638,6 +636,19 @@ class ApiUsersController < ApiController
end
end
def update_avatar_v2
url = params[:url]
# crop_selection = params[:crop_selection]
@user.update_avatar_v2(url)
if @user.errors.any?
respond_with @user, status: :unprocessable_entity
else
respond_with @user, responder: ApiResponder, status: 200
end
end
def delete_avatar
@user.delete_avatar(Rails.application.config.aws_bucket_public)
@ -648,6 +659,16 @@ class ApiUsersController < ApiController
end
end
def delete_avatar_v2
@user.delete_avatar_v2(Rails.application.config.aws_bucket_public)
if @user.errors.any?
respond_with @user, status: :unprocessable_entity
else
respond_with @user, responder: ApiResponder, status: 204
end
end
def generate_filepicker_policy
# generates a soon-expiring filepicker policy so that a user can only upload to their own folder in their bucket

View File

@ -1,7 +1,7 @@
object @profile
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :concert_count, :studio_session_count, :virtual_band, :virtual_band_commitment, :traditional_band, :traditional_band_commitment, :traditional_band_touring, :paid_sessions, :paid_sessions_hourly_rate,
:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose, :subscribe_email, :is_a_teacher, :is_a_student, :last_active_timestamp
:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose, :subscribe_email, :is_a_teacher, :is_a_student, :last_active_timestamp, :v2_photo_url, :v2_photo_uploaded
child :online_presences => :online_presences do
attributes :id, :service_type, :username

View File

@ -2,7 +2,7 @@ object @user
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :admin,
:recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :reuse_card, :email_needs_verification, :is_a_teacher, :is_a_student, :is_onboarder, :timezone,
:use_video_conferencing_server
:use_video_conferencing_server, :v2_photo_url, :v2_photo_uploaded
node :location do |user|
if user.musician?

View File

@ -509,6 +509,9 @@ Rails.application.routes.draw do
match '/users/:id/avatar' => 'api_users#delete_avatar', :via => :delete
match '/users/:id/filepicker_policy' => 'api_users#generate_filepicker_policy', :via => :get
match '/users/:id/avatar_v2' => 'api_users#update_avatar_v2', :via => :post
match '/users/:id/avatar_v2' => 'api_users#delete_avatar_v2', :via => :delete
# user progression
match '/users/progression/downloaded_client' => 'api_users#downloaded_client', :via => :post
match '/users/progression/certified_gear' => 'api_users#qualified_gear', :via => :post