mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-22 12:03:25 -04:00
logging in with mastodon
This commit is contained in:
parent
45950077fd
commit
97616e2754
@ -12,9 +12,19 @@
|
|||||||
<div class="row flex-row-reverse">
|
<div class="row flex-row-reverse">
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<div class="btn-group-vertical w-100 mb-3">
|
<div class="btn-group-vertical w-100 mb-3">
|
||||||
<a :href="`${homeUrl}/api/user/social-redirect/${provider}/${config.locale}`" v-for="(providerOptions, provider) in socialProviders" class="btn btn-outline-primary">
|
<a :href="!providerOptions.instanceRequired ? `${homeUrl}/api/user/social-redirect/${provider}/${config.locale}` : null"
|
||||||
|
:class="providerOptions.instanceRequired ? 'btn border-primary text-primary' : 'btn btn-outline-primary'"
|
||||||
|
v-for="(providerOptions, provider) in socialProviders">
|
||||||
<Icon :v="providerOptions.icon || provider" set="b"/>
|
<Icon :v="providerOptions.icon || provider" set="b"/>
|
||||||
{{ providerOptions.name }}
|
{{ providerOptions.name }}
|
||||||
|
<form :action="`${homeUrl}/api/user/social-redirect/${provider}/${config.locale}`"
|
||||||
|
v-if="providerOptions.instanceRequired" class="input-group">
|
||||||
|
<input type="text" name="instance" class="form-control"
|
||||||
|
:placeholder="$t('user.login.instancePlaceholder')">
|
||||||
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
|
<Icon v="arrow-right"/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -487,6 +487,7 @@ user:
|
|||||||
why: >
|
why: >
|
||||||
Registering lets you manage your cards ({/@example=like this one}).
|
Registering lets you manage your cards ({/@example=like this one}).
|
||||||
passwordless: 'The website doesn''t store any passwords. {https://avris.it/blog/passwords-are-passé=More info.}'
|
passwordless: 'The website doesn''t store any passwords. {https://avris.it/blog/passwords-are-passé=More info.}'
|
||||||
|
instancePlaceholder: 'Instance'
|
||||||
code:
|
code:
|
||||||
action: 'Validate'
|
action: 'Validate'
|
||||||
invalid: 'Invalid code.'
|
invalid: 'Invalid code.'
|
||||||
|
@ -1184,6 +1184,7 @@ user:
|
|||||||
why: >
|
why: >
|
||||||
Założenie konta pozwala na zarządzanie swoimi wizytówkami ({/@example=takimi jak ta}).
|
Założenie konta pozwala na zarządzanie swoimi wizytówkami ({/@example=takimi jak ta}).
|
||||||
passwordless: 'Strona nie zapisuje żadnych haseł. {https://avris.it/blog/passwords-are-passé=Więcej info.}'
|
passwordless: 'Strona nie zapisuje żadnych haseł. {https://avris.it/blog/passwords-are-passé=Więcej info.}'
|
||||||
|
instancePlaceholder: 'Instancja'
|
||||||
code:
|
code:
|
||||||
action: 'Sprawdź'
|
action: 'Sprawdź'
|
||||||
invalid: 'Kod nieprawidłowy.'
|
invalid: 'Kod nieprawidłowy.'
|
||||||
|
11
migrations/041-mastodon-oauth.sql
Normal file
11
migrations/041-mastodon-oauth.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- Up
|
||||||
|
|
||||||
|
CREATE TABLE mastodon_oauth (
|
||||||
|
instance TEXT NOT NULL PRIMARY KEY,
|
||||||
|
client_id TEXT NOT NULL,
|
||||||
|
client_secret TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Down
|
||||||
|
|
||||||
|
DROP TABLE mastodon_oauth;
|
@ -82,6 +82,7 @@ app.use(async function (req, res, next) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(require('./routes/grantOverrides').default);
|
||||||
router.use(grant.express()(require('./social').config));
|
router.use(grant.express()(require('./social').config));
|
||||||
|
|
||||||
app.use(require('./routes/home').default);
|
app.use(require('./routes/home').default);
|
||||||
|
101
server/routes/grantOverrides.js
Normal file
101
server/routes/grantOverrides.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// grant doesn't care about the specifics of some services,
|
||||||
|
// so for some services we don't care about grant :))))
|
||||||
|
|
||||||
|
import { Router } from 'express';
|
||||||
|
import SQL from 'sql-template-strings';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import assert from 'assert';
|
||||||
|
import { handleErrorAsync } from "../../src/helpers";
|
||||||
|
|
||||||
|
const normalizeDomainName = (domain) => {
|
||||||
|
const url = new URL('https://' + domain);
|
||||||
|
assert(url.port === '');
|
||||||
|
return url.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
mastodon: {
|
||||||
|
scopes: ['read:accounts'],
|
||||||
|
redirect_uri: `${process.env.HOME_URL || 'https://pronouns.page'}/api/user/social/mastodon`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
const mastodonGetOAuthKeys = async (db, instance, dbOnly = false) => {
|
||||||
|
const existingKeys = await db.get(
|
||||||
|
SQL`SELECT client_id, client_secret FROM mastodon_oauth WHERE instance = ${instance}`);
|
||||||
|
if (existingKeys) {
|
||||||
|
return existingKeys;
|
||||||
|
}
|
||||||
|
const keys = await fetch(`https://${instance}/api/v1/apps`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_name: 'pronouns.page',
|
||||||
|
redirect_uris: config.mastodon.redirect_uri,
|
||||||
|
scopes: config.mastodon.scopes.join(' '),
|
||||||
|
website: process.env.HOME_URL,
|
||||||
|
}).toString(),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'User-Agent': 'pronouns.page',
|
||||||
|
},
|
||||||
|
}).then(res => res.json());
|
||||||
|
assert(keys.client_id && keys.client_secret && !keys.error);
|
||||||
|
db.get(SQL`
|
||||||
|
INSERT INTO mastodon_oauth (instance, client_id, client_secret)
|
||||||
|
VALUES (${instance}, ${keys.client_id}, ${keys.client_secret})
|
||||||
|
`);
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
router.get('/connect/mastodon', handleErrorAsync(async (req, res) => {
|
||||||
|
assert(req.query.instance);
|
||||||
|
const instance = normalizeDomainName(req.query.instance);
|
||||||
|
const { client_id, client_secret } = await mastodonGetOAuthKeys(req.db, instance);
|
||||||
|
req.session.grant = { instance, client_id, client_secret };
|
||||||
|
res.redirect(`https://${instance}/oauth/authorize?` + new URLSearchParams({
|
||||||
|
client_id,
|
||||||
|
scope: config.mastodon.scopes.join(' '),
|
||||||
|
redirect_uri: config.mastodon.redirect_uri,
|
||||||
|
response_type: 'code',
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
router.get('/user/social/mastodon', handleErrorAsync(async (req, res, next) => {
|
||||||
|
if (!req.session.grant || !req.session.grant.instance || !req.query.code) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { instance, client_id, client_secret } = req.session.grant;
|
||||||
|
const response = await fetch(`https://${instance}/oauth/token`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
redirect_uri: config.mastodon.redirect_uri,
|
||||||
|
scope: config.mastodon.scopes.join(' '),
|
||||||
|
code: req.query.code,
|
||||||
|
}).toString(),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'User-Agent': 'pronouns.page',
|
||||||
|
},
|
||||||
|
}).then(res => res.json());
|
||||||
|
if (!response.access_token || response.error) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const profile = await fetch(`https://${instance}/api/v1/accounts/verify_credentials`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${response.access_token}`,
|
||||||
|
'User-Agent': 'pronouns.page',
|
||||||
|
},
|
||||||
|
}).then(res => res.json());
|
||||||
|
response.profile = profile;
|
||||||
|
response.instance = instance;
|
||||||
|
req.session.grant.response = response;
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default router;
|
@ -388,7 +388,9 @@ router.post('/user/:id/set-roles', handleErrorAsync(async (req, res) => {
|
|||||||
// happens on home
|
// happens on home
|
||||||
router.get('/user/social-redirect/:provider/:locale', handleErrorAsync(async (req, res) => {
|
router.get('/user/social-redirect/:provider/:locale', handleErrorAsync(async (req, res) => {
|
||||||
req.session.socialRedirect = req.params.locale;
|
req.session.socialRedirect = req.params.locale;
|
||||||
return res.redirect(`/api/connect/${req.params.provider}`);
|
return res.redirect(`/api/connect/${req.params.provider}?${new URLSearchParams({
|
||||||
|
instance: req.query.instance || undefined,
|
||||||
|
})}`);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// happens on home
|
// happens on home
|
||||||
|
@ -30,6 +30,8 @@ export const config = {
|
|||||||
callback: '/api/user/social/discord',
|
callback: '/api/user/social/discord',
|
||||||
scope: ['identify', 'email'],
|
scope: ['identify', 'email'],
|
||||||
},
|
},
|
||||||
|
// non-grant, but things break if it's not there
|
||||||
|
mastodon: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handlers = {
|
export const handlers = {
|
||||||
@ -73,4 +75,13 @@ export const handlers = {
|
|||||||
access_secret: r.access_secret,
|
access_secret: r.access_secret,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mastodon(r) {
|
||||||
|
const acct = `${r.profile.username}@${r.instance}`;
|
||||||
|
return {
|
||||||
|
id: acct,
|
||||||
|
name: acct,
|
||||||
|
avatar: r.profile.avatar,
|
||||||
|
access_token: r.access_token,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -7,6 +7,7 @@ export const socialProviders = {
|
|||||||
facebook: { name: 'Facebook' },
|
facebook: { name: 'Facebook' },
|
||||||
google: { name: 'Google' },
|
google: { name: 'Google' },
|
||||||
discord: { name: 'Discord' },
|
discord: { name: 'Discord' },
|
||||||
|
mastodon: { name: 'Mastodon', instanceRequired: true },
|
||||||
}
|
}
|
||||||
|
|
||||||
import pronounsRaw from '../data/pronouns/pronouns.tsv';
|
import pronounsRaw from '../data/pronouns/pronouns.tsv';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user