Concepteur développeur d'applications
16 mai 2022
Yann Loosli
IDENTITIES
> JWT> PASSPORT> NODEJS> EXPRESS> MONGOOSE
MONGODB
GROUPS
> NODEJS> EXPRESS
MYSQL
MESSAGES
> ???
???
MEMORY
> NODEJS> EXPRESS
MYSQL
MAIL
> MJML> NODEMAI- LER
BUBBLES
> Y.JS
EVENT BUS
> NATS
API GATEWAY
> APOLLO GRAPHQL
BACK-END
FRONT-END
WEBSITE
> REACT> REACT ROUTER> CHAKRA UI
COOKIES
WEB EXTENSION
> REACT
LOCAL STORAGE
MOBILE APPLICATION
> REACT NATIVE> EXPO
Background
Action
Option
Script contents
DevTools
+ F12
Web Ext.
Tabs
Tabs
Tabs
Tabs...
Background
Server
Action (popup)
Script Contents
DevTools
Options
LOGIN
LOGOUT
(menu ?)
1 - login
2 - JWT dans localstrage
3 - Met le JWT
dans le store REDUX
STORE REDUX
(main)
4 - WebSocket & GraphQl
Browser API
REDUX WebExt
LOCAL STORAGE
G
o
o
g
l
e
G
o
o
g
l
e
G
o
o
g
l
e
{ "manifest_version": 2, "short_name": "Pygma.Link Web Extension", "name": "Pygma.Link Web Extension", "version": "0", "description": "to do", "content_security_policy": " script-src 'self' localhost:*; object-src 'self'; connect-src 'self' ws://localhost:1236/ ws://localhost:4000/ http://localhost:4000/; ", "author": "PygmaTeam", "web_accessible_resources": [ "**/*.png", "**/*.svg", "excalidraw-assets-dev/*.*", "excalidraw-assets/*.*"], "content_scripts": [ { "matches": ["<all_urls>"], "match_about_blank": true, "css": [ "parts/scriptContent/styles/style.scss" ], "js": [ "parts/scriptContent/var.ts", "parts/scriptContent/entry.tsx" ] } ], "browser_action": { "default_icon": "./assets/logo192.png", "default_title": "PYGMA.LINK - Menu", "default_popup": "./parts/action/popup.html" }, "devtools_page": "./parts/devtools/devtools.html", "options_ui": { "page": "./parts/option/option.html" }, "background": { "scripts": ["./parts/background/entry.ts"], "persistent": true }, "permissions": [ "tabs", "webNavigation", "<all_urls>" ]}
manifest.json ( )
import React, { FC, useEffect, useState, useRef, RefObject, useLayoutEffect } from 'react';import { motion } from 'framer-motion';import { browser } from 'webextension-polyfill-ts';import App from './Draw';import SnailMenu from './SnailMenu';import '../styles/style.scss';import { CollabContextConsumer } from '../compFct/drawCollabWrapper';const Overlay: FC = () => { const [online, setOnline] = useState(true); const [drawBoard, setDrawBoard] = useState(false); const [snailMenuOpen, setSnailMenuOpen] = useState(false); const constraintsRef = useRef<RefObject<Element> & HTMLDivElement>(null); // send connexion data to background const port = browser.runtime.connect(); useEffect(() => { port.postMessage({type: 'PAGELOAD'}); const waitLogin = setInterval(() => { port.postMessage({ type: 'ISAUTH', tab: '', url: window.location.href, group: '', data: {} }); }, 1000); port.onMessage.addListener((message) => { console.log(message) if (online === false && message.type === 'ISAUTH' && message.data.state === true) { setOnline(true); clearInterval(waitLogin) } }); }, []) return ( <CollabContextConsumer> {online && ( <motion.div className="Overlay" ref={constraintsRef} style={{ opacity: online ? 1 : 0 }}> <motion.div className="Snail" drag dragPropagation dragMomentum={false} dragConstraints={constraintsRef} style={{ transformOrigin: '50% 50%', zIndex: 100, position: 'absolute', left: '50px', top: 'calc(100vh - 100px)', }} animate={ !snailMenuOpen ? { width: '70px', height: '70px', transition: { delay: 0, duration: 0, }, } : {} } > <SnailMenu setOpen={setSnailMenuOpen} open={snailMenuOpen} setDrawBoard={setDrawBoard} drawBoard={drawBoard} /> </motion.div> {drawBoard && <App />} </motion.div> )} </CollabContextConsumer> );};export default Overlay;
Overlay.tsx
import React, { ReactElement, ReactNode } from 'react';import { motion } from 'framer-motion';type SnailProps = { open: boolean; children: ReactNode;};const snailSections = [ "M111.061 140.676a4.091 4.091 0 01-5.321 2.27 4.091 4.091 0 01-2.271-5.32 4.091 4.091 0 015.321-2.272 4.091 4.091 0 012.272 5.321", "M119.693 139.429c-5.05 10.642-17.555 6.165-16.518-1.01-.305 4.438 6.379 7.599 8.03 1.057z", . . .]const snailForm = "m 103.17507,138.41902 c -0.84386,3.46277 3.21063,7.09831 6.85103,. . . "const snailFormParams = [{ stroke: '#cccccc', strokeOpacity: 0.3, }, { strokeOpacity: . . . ]const Snail = (props: SnailProps): ReactElement => { const { open, children } = props; const variants = { open: (custom: number) => ({ strokeWidth: 0.5, strokeOpacity: 1, zIndex: 13 - custom, opacity: 1, transition: { delay: custom * 0.04, ease: 'easeInOut', duration: 0.25, }, scale: [0, 1.2, 1], filter: ['blur(1px)', 'blur(.5px)', 'blur(0)'], }), closed: (custom: number) => ({ strokeWidth: 0, strokeOpacity: [1, 1, 0], opacity: [1, 1, 0], transition: { delay: custom * 0.02, ease: 'easeInOut', duration: 0.1, }, scale: [1, 1.2, 0], filter: ['blur(.25px)', 'blur(.5px)', 'blur(2px)'], }), }; return ( <> <svg width={367.681} height={410.366} viewBox="0 0 97.282 108.576" id="prefix__svg5" xmlns="http://www.w3.org/2000/svg" {...props} style={{ position: 'absolute', }} > <defs> <linearGradient id="linear" x1="138.17851" y1="182.58768" x2="68.166489" y2="100.48698"> <stop offset="0%" stopColor="#cacaca" /> <stop offset="100%" stopColor="#f0f0f0" /> </linearGradient> </defs> <g id="snail" transform="translate(-54.794 -87.071)" fillOpacity={1} fill="url(#linear)" stroke="url(#linear)"> <g> {snailSections.map((section, index) => ( <motion.path key={`snailPart_${index}`} id={`snailPart_${index}`} d={section} fill="url(#linear)" opacity={0.9} animate={open ? 'open' : 'closed'} variants={variants} custom={open ? index : section.length - index} /> ))} </g> </g> <g transform="translate(-54.794 -87.071)"> { snailFormParams.map((el, index) => ( <motion.path id={`path_${index}`} key={`path_${index}`} style={{ fill: 'none', stroke: el.stroke || '#000', strokeWidth: el.strokeWidth || '0.25px', strokeLinecap: 'butt', strokeLinejoin: 'miter', strokeOpacity: el.strokeOpacity || 1, filter: el.filter || '', scale: el.scale || 1, }} d={snailForm} initial={{ pathLength: open ? 0 : 1 }} animate={{ pathLength: open ? 1 : 0, transition: { ease: 'easeInOut', duration: open ? 0.5 : 1, }, }} /> )) } </g> </svg> {children} </> );};export default Snail;
Snail.tsx
?
?
?
?
> connexion <
inscription
présentation
> connexion <
inscription
présentation
Besoin d'aide ?
- Assistance
- Téléchargement
- FAQ
Button
Valider
CONNEXION
INSCRIPTION
mail
mail
mdp
mdp
mdp bis
CGU ?
CGV ?
RGPD ?
export const userSchema = new Schema( { mail: {type: String, unique: true}, password: String, status: { type: String, enum: ['Pending', 'Active'], default: 'Pending' }, confirmationProcess: [confirmationProcess], contractAgreement: [{ docId: String, version: String, validationDate: {type: Number, default: Math.floor(Date.now() / 1000)} }], paymentHistory: [{ }], profils: [{ nickname: {type: String, unique: true}, groups: [{ id: String }] }], type: { type: String, default: 'free', enum: ['free', 'star', 'premium', 'pro', 'pro+'], }, typeStartDate: {type: Number, default: Math.floor(Date.now() / 1000)}, typeEndDate: {type: Number, default: Math.floor(Date.now() / 1000)}, pages: [{ id: String, title: String, creationDate: {type: Number, default: Math.floor(Date.now() / 1000)}, enabled: {type: Boolean, default: false}, accessibility: [String] }], loggedOutAt: Number, createdAt: Number, updatedAt: Number, }, { timestamps: { currentTime: () => Math.floor(Date.now() / 1000) } });
Schéma Mongoose USER
1 const LOGIN = gql`2 query login($mail: String, $password: String) {3 login(mail: $mail, password: $password) {4 _id5 }6 }7 `
La requête GraphQL
1 const login = async (parent, args, context, info) => { 2 const result = await post('login', args) 3 if (!result) 4 return { 5 success: false, 6 message: 'failed to login', 7 } 8 9 context.setCookies.push({10 name: 'jwt_sign',11 value: result.data.jwt_sign,12 options: {13 domain: 'localhost',14 httpOnly: true,15 path: '/',16 sameSite: true,17 secure: true,18 },19 })20 context.setHeaders.push({ key: 'jwt_data', value: result.data.jwt_data })21 22 return {23 _id: result.data._id,24 }25 }
Depuis le service « API »
1 export default (router: Router): void => { 2 3 router.post( 4 '/login', 5 (request, response) => { 6 passport.authenticate("local",{session: false}, (err, user, info) => { 7 if (err || !user) { 8 return response.json({ 9 message: info,10 user : user11 });12 }13 request.login(user, {session: false}, (err) => {14 if (err) {15 response.send(err);16 }17 18 const payload = { 19 id: user._doc._id,20 type: user._doc.type,21 }22 23 const token = jwt.sign(payload, process.env.JWT_SECRET)24 const tokenSplit = token.split('.')25 26 27 nats('LOGIN', JSON.stringify({28 id: user._doc._id,29 type: user._doc.type,30 mail: user._doc.mail,31 }))32 33 return response.json({34 ...user._doc,35 jwt_sign: tokenSplit[2],36 jwt_data: tokenSplit[0] + '.' + tokenSplit[1]37 });38 }) 39 }) (request, response)40 }41 )42 }
Depuis le service « identities »
1 context.setCookies.push({ 2 name: 'jwt_sign', 3 value: result.data.jwt_sign, 4 options: { 5 domain: 'localhost', 6 httpOnly: true, 7 path: '/', 8 sameSite: true, 9 secure: true,10 },11 })12 context.setHeaders.push({ key: 'jwt_data', value: result.data.jwt_data })
Envoi des informations au client dans le header
1 if (req.headers.cookie && req.headers.cookie.length > 0) { 2 let cookiesRaw = req.headers.cookie?.split('; ') 3 cookiesRaw = cookiesRaw?.filter(cookie => cookie.trim().slice(0, 3) === 'jwt') 4 if (cookiesRaw.length === 2) { 5 let tokenWip = { jwt_data: '', jwt_sign: '' } 6 cookiesRaw?.forEach(cookie => { 7 const pieces = cookie.split('=') 8 tokenWip = { ...tokenWip, [pieces[0]]: pieces[1] } 9 })10 if (tokenWip.jwt_data !== (null || undefined)) {11 const token = `${tokenWip.jwt_data}.${tokenWip.jwt_sign}` || ''12 userId = jwt.verify(token, jwtSecret)13 ? JSON.parse(atob(tokenWip.jwt_data.split('.')[1])).id14 : ''15 }16 }17 }
Réception de la requête du client
1 import React from 'react'; 2 import {View} from 'react-native'; 3 4 function HomeScreen() { 5 return ( 6 < View /> 7 ); 8 } 9 10 export default HomeScreen;
Etape 1 : placeholder
1 import React from 'react'; 2 import {render} from '@testing-library/react-native'; 3 import HomeScreen from '../HomeScreen'; 4 5 describe('HomeScreen', () => { 6 test('should render correctly', () => { 7 const wrapper = render(GlobalWrapper(<HomeScreen />)); 8 wrapper.getByTestId('home-screen'); 9 });10 });
Etape 2 : mise en place du test
1 import React from 'react'; 2 import {View} from 'react-native'; 3 4 function HomeScreen() { 5 return ( 6 < View testID="home-screen" /> 7 ); 8 } 9 10 export default HomeScreen;
Etape 3 : compléter le code pour répondre au test
1 import React from 'react'; 2 import {Flex} from 'native-base'; 3 import Logo from '../components/Logo'; 4 import LoginForm from '../components/LoginForm'; 5 6 function HomeScreen() { 7 return ( 8 <Flex 9 testID="home-screen"10 bg={{11 linearGradient: {12 colors: ['pygmaBlue.300', 'dark.900', 'pygmaOrange.500'],13 start: [0, 0],14 end: [1, 0],15 },16 }}17 h="100%"18 alignItems="center"19 justifyContent="space-around">20 <Logo />21 <LoginForm />22 </Flex>23 );24 }
Etape 5 : le code final
1 import React from 'react'; 2 import {render} from '@testing-library/react-native'; 3 import LoginForm from '../../components/LoginForm'; 4 import HomeScreen from '../HomeScreen'; 5 import {View} from 'react-native'; 6 import GlobalWrapper from '../../fixtures/GlobalWrapper'; 7 8 jest.mock('../../components/LoginForm', () => jest.fn().mockReturnValue(null)); 9 10 describe('HomeScreen', () => {11 test('should render correctly', () => {12 const wrapper = render(GlobalWrapper(<HomeScreen />));13 wrapper.getByTestId('home-screen');14 });15 16 test('should have a logo', () => {17 const wrapper = render(GlobalWrapper(<HomeScreen />));18 wrapper.getByTestId('logo');19 });20 21 test('should have a login form', () => {22 (LoginForm as jest.Mock).mockReturnValue(<View testID="mock-login-form" />);23 const wrapper = render(GlobalWrapper(<HomeScreen />));24 wrapper.getByTestId('mock-login-form');25 });26 });
Etape 4 : le test final
1 variables: 2 DEV_PATH: /var/www/entreloups.com/kivi1 3 4 stages: 5 - dev 6 7 dev: 8 stage: dev 9 only:10 - /^release\/dev$/11 tags:12 - kivi1dev13 script:14 - cp $DEV_PATH/.env.local .15 - cp $DEV_PATH/.env.prod.local .16 - test -f composer.lock && rm composer.lock17 - test -d vendor && rm vendor -rf18 - test -d node_modules && rm node_modules -rf19 - /usr/bin/php /usr/local/bin/composer install20 - /usr/bin/yarn21 - /usr/bin/php bin/console fos:js-routing:dump --format=json –target=public/js/fos_js_routes.json22 - /usr/bin/yarn build23 - cd $DEV_PATH24 - /usr/bin/php bin/console doctrine:cache:clear-metadata25 - /usr/bin/php bin/console doctrine:cache:clear-query26 - /usr/bin/php bin/console doctrine:cache:clear-result27 - /usr/bin/php bin/console doctrine:migrations:migrate -n28 - /usr/bin/php bin/console cache:clear
Extraits du fichier de déploiement .gitlab-ci.yml :
push
dépôt
local
distant
clone
serveurpreprod
dépôt
gitlabrunner
Gitlab
version: '3'services: apache-php: build: .docker/dev container_name: kivi1_apache_php working_dir: /usr/app/ ports: - "80:80" - "25:25" volumes: - ./node_modules:/usr/app/node_modules - ./vendor:/usr/app/vendor - ./antivirus:/usr/app/antivirus - ./assets:/usr/app/assets - ./src:/usr/app/src - ./bin:/usr/app/bin - ./client:/usr/app/client - ./config:/usr/app/config - ./migrations:/usr/app/migrations - ./public:/usr/app/public - ./templates:/usr/app/templates - ./translations:/usr/app/translations - ./package.json:/usr/app/package.json - ./composer.json:/usr/app/composer.json - ./tsconfig.json:/usr/app/tsconfig.json - ./.babelrc:/usr/app/.babelrc - ./.env:/usr/app/.env - ./.env.dev:/usr/app/.env.dev - ./.env.dev.local:/usr/app/.env.dev.local - ./webpack.config.js:/usr/app/webpack.config.js - ./yarn.lock:/usr/app/yarn.lock depends_on: - mysql command: - apache2-foreground & composer install && php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json && php bin/console c:cl && php bin/console d:m:m && yarn && yarn watch mysql: image: mysql:5.7 container_name: kivi1_mysql command: "--default-authentication-plugin=mysql_native_password" ports: - 3306 volumes: - .docker/dev/data/db:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: nOts0secur3d MYSQL_DATABASE: kivi1 MYSQL_USER: kivi1Admin MYSQL_PASSWORD: nOtMuchs0secured31ther
docker-compose-dev.yml :
FROM php:8.1-apacheRUN a2enmod rewrite[ . . . ]## Install composer globallyRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer -d=/var/www[ . . . ]#NPM / nodeRUN apt-get install -y curl \ && curl -sL https://deb.nodesource.com/setup_12.x | bash - \ && apt-get install -y nodejs \ && curl -L https://www.npmjs.com/install.sh | shENV PATH $PATH:/node_modules/.binRUN npm install --global yarn[ . . . ]COPY xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.iniCOPY php.ini /usr/local/etc/php/php.iniCOPY error_reporting.ini /usr/local/etc/php/conf.d/error_reporting.iniCOPY kivi1.dev.conf /etc/apache2/sites-available/000-default.conf ENTRYPOINT [ "/bin/sh", "-c" ]
extrait de dockerfile
hôte
conteneur
volumes