WIP to run without mailgun and social logins
This commit is contained in:
parent
cf0cd38269
commit
bd8391bcf4
11 changed files with 147 additions and 117 deletions
4
app.json
4
app.json
|
@ -38,6 +38,10 @@
|
||||||
"description": "A secret key for...? Not sure where used.",
|
"description": "A secret key for...? Not sure where used.",
|
||||||
"generator": "secret"
|
"generator": "secret"
|
||||||
},
|
},
|
||||||
|
"EXAMPLE_USERNAME": {
|
||||||
|
"description": "Username of the default account.",
|
||||||
|
"value": "p5"
|
||||||
|
},
|
||||||
"EXAMPLE_USER_EMAIL": {
|
"EXAMPLE_USER_EMAIL": {
|
||||||
"description": "The email address for the account holding the default Example sketches",
|
"description": "The email address for the account holding the default Example sketches",
|
||||||
"value": "examples@p5js.org"
|
"value": "examples@p5js.org"
|
||||||
|
|
|
@ -321,11 +321,18 @@ class Editor extends React.Component {
|
||||||
'editor--options': this.props.editorOptionsVisible
|
'editor--options': this.props.editorOptionsVisible
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(this.props.file);
|
||||||
const editorHolderClass = classNames({
|
const editorHolderClass = classNames({
|
||||||
'editor-holder': true,
|
'editor-holder': true,
|
||||||
'editor-holder--hidden': this.props.file.fileType === 'folder' || this.props.file.url
|
'editor-holder--hidden': this.props.file.fileType === 'folder' || this.props.file.url
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let preview = '';
|
||||||
|
if (this.props.file.fileType === 'file' && this.props.file.url) {
|
||||||
|
// TODO check if it's an image
|
||||||
|
preview = (<div><img src={this.props.file.url} alt="preview" /></div>);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={editorSectionClass} >
|
<section className={editorSectionClass} >
|
||||||
<header className="editor__header">
|
<header className="editor__header">
|
||||||
|
@ -360,6 +367,7 @@ class Editor extends React.Component {
|
||||||
</header>
|
</header>
|
||||||
<article ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
|
<article ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
|
||||||
</article>
|
</article>
|
||||||
|
{preview}
|
||||||
<EditorAccessibility
|
<EditorAccessibility
|
||||||
lintMessages={this.props.lintMessages}
|
lintMessages={this.props.lintMessages}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -22,7 +22,7 @@ import FooterTabSwitcher from '../../components/mobile/TabSwitcher';
|
||||||
import FooterTab from '../../components/mobile/Tab';
|
import FooterTab from '../../components/mobile/Tab';
|
||||||
import Loader from '../App/components/loader';
|
import Loader from '../App/components/loader';
|
||||||
|
|
||||||
const EXAMPLE_USERNAME = 'p5';
|
const EXAMPLE_USERNAME = process.env.EXAMPLE_USERNAME || 'p5';
|
||||||
|
|
||||||
// @ghalestrilo 08/13/2020: I'm sorry
|
// @ghalestrilo 08/13/2020: I'm sorry
|
||||||
const ContentWrapper = styled(Content)`
|
const ContentWrapper = styled(Content)`
|
||||||
|
|
|
@ -60,7 +60,7 @@ class AccountView extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<SocialLoginPanel {...this.props} />
|
{/* <SocialLoginPanel {...this.props} /> */}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<APIKeyForm {...this.props} />
|
<APIKeyForm {...this.props} />
|
||||||
|
|
|
@ -42,11 +42,11 @@ class LoginView extends React.Component {
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">{this.props.t('LoginView.Login')}</h2>
|
<h2 className="form-container__title">{this.props.t('LoginView.Login')}</h2>
|
||||||
<LoginForm {...this.props} />
|
<LoginForm {...this.props} />
|
||||||
<h2 className="form-container__divider">{this.props.t('LoginView.LoginOr')}</h2>
|
{/* <h2 className="form-container__divider">{this.props.t('LoginView.LoginOr')}</h2>
|
||||||
<div className="form-container__stack">
|
<div className="form-container__stack">
|
||||||
<SocialAuthButton service={SocialAuthButton.services.github} />
|
<SocialAuthButton service={SocialAuthButton.services.github} />
|
||||||
<SocialAuthButton service={SocialAuthButton.services.google} />
|
<SocialAuthButton service={SocialAuthButton.services.google} />
|
||||||
</div>
|
</div> */}
|
||||||
<p className="form__navigation-options">
|
<p className="form__navigation-options">
|
||||||
{this.props.t('LoginView.DontHaveAccount')}
|
{this.props.t('LoginView.DontHaveAccount')}
|
||||||
<Link className="form__signup-button" to="/signup">{this.props.t('LoginView.SignUp')}</Link>
|
<Link className="form__signup-button" to="/signup">{this.props.t('LoginView.SignUp')}</Link>
|
||||||
|
|
|
@ -34,11 +34,11 @@ class SignupView extends React.Component {
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">{this.props.t('SignupView.Description')}</h2>
|
<h2 className="form-container__title">{this.props.t('SignupView.Description')}</h2>
|
||||||
<SignupForm {...this.props} />
|
<SignupForm {...this.props} />
|
||||||
<h2 className="form-container__divider">{this.props.t('SignupView.Or')}</h2>
|
{/* <h2 className="form-container__divider">{this.props.t('SignupView.Or')}</h2>
|
||||||
<div className="form-container__stack">
|
<div className="form-container__stack">
|
||||||
<SocialAuthButton service={SocialAuthButton.services.github} />
|
<SocialAuthButton service={SocialAuthButton.services.github} />
|
||||||
<SocialAuthButton service={SocialAuthButton.services.google} />
|
<SocialAuthButton service={SocialAuthButton.services.google} />
|
||||||
</div>
|
</div> */}
|
||||||
<p className="form__navigation-options">
|
<p className="form__navigation-options">
|
||||||
{this.props.t('SignupView.AlreadyHave')}
|
{this.props.t('SignupView.AlreadyHave')}
|
||||||
<Link className="form__login-button" to="/login">{this.props.t('SignupView.Login')}</Link>
|
<Link className="form__login-button" to="/login">{this.props.t('SignupView.Login')}</Link>
|
||||||
|
|
2
index.js
2
index.js
|
@ -1,3 +1,5 @@
|
||||||
|
console.log('environment:',process.env.NODE_ENV);
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
process.env.webpackAssets = JSON.stringify(require('./dist/static/manifest.json'));
|
process.env.webpackAssets = JSON.stringify(require('./dist/static/manifest.json'));
|
||||||
require('./dist/server.bundle.js');
|
require('./dist/server.bundle.js');
|
||||||
|
|
|
@ -3,9 +3,9 @@ import friendlyWords from 'friendly-words';
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
|
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
import GitHubStrategy from 'passport-github';
|
// import GitHubStrategy from 'passport-github';
|
||||||
import LocalStrategy from 'passport-local';
|
import LocalStrategy from 'passport-local';
|
||||||
import GoogleStrategy from 'passport-google-oauth20';
|
// import GoogleStrategy from 'passport-google-oauth20';
|
||||||
import { BasicStrategy } from 'passport-http';
|
import { BasicStrategy } from 'passport-http';
|
||||||
|
|
||||||
import User from '../models/user';
|
import User from '../models/user';
|
||||||
|
@ -82,101 +82,101 @@ const getPrimaryEmail = githubEmails => (
|
||||||
/**
|
/**
|
||||||
* Sign in with GitHub.
|
* Sign in with GitHub.
|
||||||
*/
|
*/
|
||||||
passport.use(new GitHubStrategy({
|
// passport.use(new GitHubStrategy({
|
||||||
clientID: process.env.GITHUB_ID,
|
// clientID: process.env.GITHUB_ID,
|
||||||
clientSecret: process.env.GITHUB_SECRET,
|
// clientSecret: process.env.GITHUB_SECRET,
|
||||||
callbackURL: '/auth/github/callback',
|
// callbackURL: '/auth/github/callback',
|
||||||
passReqToCallback: true,
|
// passReqToCallback: true,
|
||||||
scope: ['user:email'],
|
// scope: ['user:email'],
|
||||||
}, (req, accessToken, refreshToken, profile, done) => {
|
// }, (req, accessToken, refreshToken, profile, done) => {
|
||||||
User.findOne({ github: profile.id }, (findByGithubErr, existingUser) => {
|
// User.findOne({ github: profile.id }, (findByGithubErr, existingUser) => {
|
||||||
if (existingUser) {
|
// if (existingUser) {
|
||||||
done(null, existingUser);
|
// done(null, existingUser);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const emails = getVerifiedEmails(profile.emails);
|
// const emails = getVerifiedEmails(profile.emails);
|
||||||
const primaryEmail = getPrimaryEmail(profile.emails);
|
// const primaryEmail = getPrimaryEmail(profile.emails);
|
||||||
|
|
||||||
User.findByEmail(emails, (findByEmailErr, existingEmailUser) => {
|
// User.findByEmail(emails, (findByEmailErr, existingEmailUser) => {
|
||||||
if (existingEmailUser) {
|
// if (existingEmailUser) {
|
||||||
existingEmailUser.email = existingEmailUser.email || primaryEmail;
|
// existingEmailUser.email = existingEmailUser.email || primaryEmail;
|
||||||
existingEmailUser.github = profile.id;
|
// existingEmailUser.github = profile.id;
|
||||||
existingEmailUser.username = existingEmailUser.username || profile.username;
|
// existingEmailUser.username = existingEmailUser.username || profile.username;
|
||||||
existingEmailUser.tokens.push({ kind: 'github', accessToken });
|
// existingEmailUser.tokens.push({ kind: 'github', accessToken });
|
||||||
existingEmailUser.name = existingEmailUser.name || profile.displayName;
|
// existingEmailUser.name = existingEmailUser.name || profile.displayName;
|
||||||
existingEmailUser.verified = User.EmailConfirmation.Verified;
|
// existingEmailUser.verified = User.EmailConfirmation.Verified;
|
||||||
existingEmailUser.save(saveErr => done(null, existingEmailUser));
|
// existingEmailUser.save(saveErr => done(null, existingEmailUser));
|
||||||
} else {
|
// } else {
|
||||||
const user = new User();
|
// const user = new User();
|
||||||
user.email = primaryEmail;
|
// user.email = primaryEmail;
|
||||||
user.github = profile.id;
|
// user.github = profile.id;
|
||||||
user.username = profile.username;
|
// user.username = profile.username;
|
||||||
user.tokens.push({ kind: 'github', accessToken });
|
// user.tokens.push({ kind: 'github', accessToken });
|
||||||
user.name = profile.displayName;
|
// user.name = profile.displayName;
|
||||||
user.verified = User.EmailConfirmation.Verified;
|
// user.verified = User.EmailConfirmation.Verified;
|
||||||
user.save(saveErr => done(null, user));
|
// user.save(saveErr => done(null, user));
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Sign in with Google.
|
// * Sign in with Google.
|
||||||
*/
|
// */
|
||||||
passport.use(new GoogleStrategy({
|
// passport.use(new GoogleStrategy({
|
||||||
clientID: process.env.GOOGLE_ID,
|
// clientID: process.env.GOOGLE_ID,
|
||||||
clientSecret: process.env.GOOGLE_SECRET,
|
// clientSecret: process.env.GOOGLE_SECRET,
|
||||||
callbackURL: '/auth/google/callback',
|
// callbackURL: '/auth/google/callback',
|
||||||
passReqToCallback: true,
|
// passReqToCallback: true,
|
||||||
scope: ['openid email'],
|
// scope: ['openid email'],
|
||||||
}, (req, accessToken, refreshToken, profile, done) => {
|
// }, (req, accessToken, refreshToken, profile, done) => {
|
||||||
User.findOne({ google: profile._json.emails[0].value }, (findByGoogleErr, existingUser) => {
|
// User.findOne({ google: profile._json.emails[0].value }, (findByGoogleErr, existingUser) => {
|
||||||
if (existingUser) {
|
// if (existingUser) {
|
||||||
done(null, existingUser);
|
// done(null, existingUser);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const primaryEmail = profile._json.emails[0].value;
|
// const primaryEmail = profile._json.emails[0].value;
|
||||||
|
|
||||||
User.findByEmail(primaryEmail, (findByEmailErr, existingEmailUser) => {
|
// User.findByEmail(primaryEmail, (findByEmailErr, existingEmailUser) => {
|
||||||
let username = profile._json.emails[0].value.split('@')[0];
|
// let username = profile._json.emails[0].value.split('@')[0];
|
||||||
User.findByUsername(username, (findByUsernameErr, existingUsernameUser) => {
|
// User.findByUsername(username, (findByUsernameErr, existingUsernameUser) => {
|
||||||
if (existingUsernameUser) {
|
// if (existingUsernameUser) {
|
||||||
const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)];
|
// const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)];
|
||||||
username = slugify(`${username} ${adj}`);
|
// username = slugify(`${username} ${adj}`);
|
||||||
}
|
// }
|
||||||
// what if a username is already taken from the display name too?
|
// // what if a username is already taken from the display name too?
|
||||||
// then, append a random friendly word?
|
// // then, append a random friendly word?
|
||||||
if (existingEmailUser) {
|
// if (existingEmailUser) {
|
||||||
existingEmailUser.email = existingEmailUser.email || primaryEmail;
|
// existingEmailUser.email = existingEmailUser.email || primaryEmail;
|
||||||
existingEmailUser.google = profile._json.emails[0].value;
|
// existingEmailUser.google = profile._json.emails[0].value;
|
||||||
existingEmailUser.username = existingEmailUser.username || username;
|
// existingEmailUser.username = existingEmailUser.username || username;
|
||||||
existingEmailUser.tokens.push({ kind: 'google', accessToken });
|
// existingEmailUser.tokens.push({ kind: 'google', accessToken });
|
||||||
existingEmailUser.name = existingEmailUser.name || profile._json.displayName;
|
// existingEmailUser.name = existingEmailUser.name || profile._json.displayName;
|
||||||
existingEmailUser.verified = User.EmailConfirmation.Verified;
|
// existingEmailUser.verified = User.EmailConfirmation.Verified;
|
||||||
existingEmailUser.save((saveErr) => {
|
// existingEmailUser.save((saveErr) => {
|
||||||
if (saveErr) {
|
// if (saveErr) {
|
||||||
console.log(saveErr);
|
// console.log(saveErr);
|
||||||
}
|
// }
|
||||||
done(null, existingEmailUser);
|
// done(null, existingEmailUser);
|
||||||
});
|
// });
|
||||||
} else {
|
// } else {
|
||||||
const user = new User();
|
// const user = new User();
|
||||||
user.email = primaryEmail;
|
// user.email = primaryEmail;
|
||||||
user.google = profile._json.emails[0].value;
|
// user.google = profile._json.emails[0].value;
|
||||||
user.username = username;
|
// user.username = username;
|
||||||
user.tokens.push({ kind: 'google', accessToken });
|
// user.tokens.push({ kind: 'google', accessToken });
|
||||||
user.name = profile._json.displayName;
|
// user.name = profile._json.displayName;
|
||||||
user.verified = User.EmailConfirmation.Verified;
|
// user.verified = User.EmailConfirmation.Verified;
|
||||||
user.save((saveErr) => {
|
// user.save((saveErr) => {
|
||||||
if (saveErr) {
|
// if (saveErr) {
|
||||||
console.log(saveErr);
|
// console.log(saveErr);
|
||||||
}
|
// }
|
||||||
done(null, user);
|
// done(null, user);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
}));
|
// }));
|
||||||
|
|
|
@ -90,6 +90,18 @@ app.use(
|
||||||
app.use(Express.static(path.resolve(__dirname, '../dist/static'), {
|
app.use(Express.static(path.resolve(__dirname, '../dist/static'), {
|
||||||
maxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === 'production' ? '1d' : '0')
|
maxAge: process.env.STATIC_MAX_AGE || (process.env.NODE_ENV === 'production' ? '1d' : '0')
|
||||||
}));
|
}));
|
||||||
|
app.use(
|
||||||
|
'/assets',
|
||||||
|
Express.static(
|
||||||
|
path.resolve(__dirname, '../dist/static/assets'),
|
||||||
|
{
|
||||||
|
// Browsers must revalidate for changes to the locale files
|
||||||
|
// It doesn't actually mean "don't cache this file"
|
||||||
|
// See: https://jakearchibald.com/2016/caching-best-practices/
|
||||||
|
setHeaders: res => res.setHeader('Cache-Control', 'no-cache')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
|
@ -135,15 +147,15 @@ app.use('/', serverRoutes);
|
||||||
app.use(assetRoutes);
|
app.use(assetRoutes);
|
||||||
|
|
||||||
app.use('/', embedRoutes);
|
app.use('/', embedRoutes);
|
||||||
app.get('/auth/github', passport.authenticate('github'));
|
// app.get('/auth/github', passport.authenticate('github'));
|
||||||
app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), (req, res) => {
|
// app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), (req, res) => {
|
||||||
res.redirect('/');
|
// res.redirect('/');
|
||||||
});
|
// });
|
||||||
|
|
||||||
app.get('/auth/google', passport.authenticate('google'));
|
// app.get('/auth/google', passport.authenticate('google'));
|
||||||
app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/login' }), (req, res) => {
|
// app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/login' }), (req, res) => {
|
||||||
res.redirect('/');
|
// res.redirect('/');
|
||||||
});
|
// });
|
||||||
|
|
||||||
// configure passport
|
// configure passport
|
||||||
require('./config/passport');
|
require('./config/passport');
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import mg from 'nodemailer-mailgun-transport';
|
// import mg from 'nodemailer-mailgun-transport';
|
||||||
|
|
||||||
const auth = {
|
const auth = {
|
||||||
api_key: process.env.MAILGUN_KEY,
|
api_key: process.env.MAILGUN_KEY,
|
||||||
|
@ -12,7 +12,10 @@ const auth = {
|
||||||
|
|
||||||
class Mail {
|
class Mail {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.client = nodemailer.createTransport(mg({ auth }));
|
this.client = nodemailer.createTransport({
|
||||||
|
streamTransport: true,
|
||||||
|
// newline: 'windows'
|
||||||
|
});
|
||||||
this.sendOptions = {
|
this.sendOptions = {
|
||||||
from: process.env.EMAIL_SENDER,
|
from: process.env.EMAIL_SENDER,
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,6 +29,7 @@ export function renderIndex() {
|
||||||
window.process.env.CLIENT = true;
|
window.process.env.CLIENT = true;
|
||||||
window.process.env.LOGIN_ENABLED = ${process.env.LOGIN_ENABLED === 'false' ? false : true};
|
window.process.env.LOGIN_ENABLED = ${process.env.LOGIN_ENABLED === 'false' ? false : true};
|
||||||
window.process.env.EXAMPLES_ENABLED = ${process.env.EXAMPLES_ENABLED === 'false' ? false : true};
|
window.process.env.EXAMPLES_ENABLED = ${process.env.EXAMPLES_ENABLED === 'false' ? false : true};
|
||||||
|
window.process.env.EXAMPLE_USERNAME = '${process.env.EXAMPLE_USERNAME || 'p5'}';
|
||||||
window.process.env.UI_ACCESS_TOKEN_ENABLED = ${process.env.UI_ACCESS_TOKEN_ENABLED === 'false' ? false : true};
|
window.process.env.UI_ACCESS_TOKEN_ENABLED = ${process.env.UI_ACCESS_TOKEN_ENABLED === 'false' ? false : true};
|
||||||
window.process.env.UI_COLLECTIONS_ENABLED = ${process.env.UI_COLLECTIONS_ENABLED === 'false' ? false : true};
|
window.process.env.UI_COLLECTIONS_ENABLED = ${process.env.UI_COLLECTIONS_ENABLED === 'false' ? false : true};
|
||||||
window.process.env.UPLOAD_LIMIT = ${process.env.UPLOAD_LIMIT ? `${process.env.UPLOAD_LIMIT}` : undefined};
|
window.process.env.UPLOAD_LIMIT = ${process.env.UPLOAD_LIMIT ? `${process.env.UPLOAD_LIMIT}` : undefined};
|
||||||
|
@ -42,13 +43,13 @@ export function renderIndex() {
|
||||||
</div>
|
</div>
|
||||||
<script src='${process.env.NODE_ENV === 'production' ? `${assetsManifest['/app.js']}` : '/app.js'}'></script>
|
<script src='${process.env.NODE_ENV === 'production' ? `${assetsManifest['/app.js']}` : '/app.js'}'></script>
|
||||||
<script>
|
<script>
|
||||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
// (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
// (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
// m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
// })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||||
|
|
||||||
ga('create', 'UA-53383000-1', 'auto');
|
// ga('create', 'UA-53383000-1', 'auto');
|
||||||
ga('send', 'pageview');
|
// ga('send', 'pageview');
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Reference in a new issue