mirror of
https://github.com/sudoxnym/habitica-self-host.git
synced 2026-07-14 10:12:21 +00:00
Merge branch 'develop' into sabrecat/teams-rebase
This commit is contained in:
commit
90375e3bc4
23 changed files with 369 additions and 100 deletions
|
|
@ -1,3 +1,7 @@
|
|||
# Files not included in deployments to Heroku, to save on file size.
|
||||
|
||||
/habitica-images
|
||||
/test
|
||||
/migrations
|
||||
/scripts
|
||||
/database_reports
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
|
|
@ -9,15 +10,18 @@ describe('GET /user/auth/apple', () => {
|
|||
let api;
|
||||
let user;
|
||||
const appleEndpoint = '/user/auth/apple';
|
||||
|
||||
before(async () => {
|
||||
const expectedResult = { id: 'appleId', name: 'an apple user' };
|
||||
sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
|
||||
});
|
||||
let randomAppleId = '123456';
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
user = await generateUser();
|
||||
randomAppleId = generateUUID();
|
||||
const expectedResult = { id: randomAppleId, name: 'an apple user' };
|
||||
sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
appleAuth.appleProfile.restore();
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
|
|
@ -26,7 +30,7 @@ describe('GET /user/auth/apple', () => {
|
|||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal('appleId');
|
||||
await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal(randomAppleId);
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('an apple user');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import passport from 'passport';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
|
|
@ -10,14 +11,15 @@ describe('POST /user/auth/social', () => {
|
|||
let api;
|
||||
let user;
|
||||
const endpoint = '/user/auth/social';
|
||||
const randomAccessToken = '123456';
|
||||
const facebookId = 'facebookId';
|
||||
const googleId = 'googleId';
|
||||
let randomAccessToken = '123456';
|
||||
let randomFacebookId = 'facebookId';
|
||||
let randomGoogleId = 'googleId';
|
||||
let network = 'NoNetwork';
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
user = await generateUser();
|
||||
randomAccessToken = generateUUID();
|
||||
});
|
||||
|
||||
it('fails if network is not supported', async () => {
|
||||
|
|
@ -32,12 +34,23 @@ describe('POST /user/auth/social', () => {
|
|||
});
|
||||
|
||||
describe('facebook', () => {
|
||||
before(async () => {
|
||||
const expectedResult = { id: facebookId, displayName: 'a facebook user' };
|
||||
beforeEach(async () => {
|
||||
randomFacebookId = generateUUID();
|
||||
const expectedResult = {
|
||||
id: randomFacebookId,
|
||||
displayName: 'a facebook user',
|
||||
emails: [
|
||||
{ value: `${user.auth.local.username}+facebook@example.com` },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
|
||||
network = 'facebook';
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
passport._strategies.facebook.userProfile.restore();
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
|
|
@ -51,7 +64,8 @@ describe('POST /user/auth/social', () => {
|
|||
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
|
||||
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(facebookId);
|
||||
await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+facebook@example.com`);
|
||||
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(randomFacebookId);
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
|
|
@ -68,6 +82,57 @@ describe('POST /user/auth/social', () => {
|
|||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.newUser).to.be.false;
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
});
|
||||
|
||||
it('logs an existing user in if they have local auth with matching email', async () => {
|
||||
passport._strategies.facebook.userProfile.restore();
|
||||
const expectedResult = {
|
||||
id: randomFacebookId,
|
||||
displayName: 'a facebook user',
|
||||
emails: [
|
||||
{ value: user.auth.local.email },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
expect(response.id).to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('logs an existing user into their social account if they have local auth with matching email', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
// This is important for existing accounts before the new social handling
|
||||
passport._strategies.facebook.userProfile.restore();
|
||||
const expectedResult = {
|
||||
id: randomFacebookId,
|
||||
displayName: 'a facebook user',
|
||||
emails: [
|
||||
{ value: user.auth.local.email },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.apiToken).not.to.eql(user.apiToken);
|
||||
expect(response.id).not.to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
|
|
@ -76,11 +141,28 @@ describe('POST /user/auth/social', () => {
|
|||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
expect(response.id).to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('does not log into other account if social auth already exists', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
|
||||
await expect(user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('socialAlreadyExists'),
|
||||
});
|
||||
});
|
||||
|
||||
xit('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
|
|
@ -92,12 +174,23 @@ describe('POST /user/auth/social', () => {
|
|||
});
|
||||
|
||||
describe('google', () => {
|
||||
before(async () => {
|
||||
const expectedResult = { id: googleId, displayName: 'a google user' };
|
||||
beforeEach(async () => {
|
||||
randomGoogleId = generateUUID();
|
||||
const expectedResult = {
|
||||
id: randomGoogleId,
|
||||
displayName: 'a google user',
|
||||
emails: [
|
||||
{ value: `${user.auth.local.username}+google@example.com` },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
|
||||
network = 'google';
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
passport._strategies.google.userProfile.restore();
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
|
|
@ -107,7 +200,8 @@ describe('POST /user/auth/social', () => {
|
|||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(googleId);
|
||||
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(randomGoogleId);
|
||||
await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+google@example.com`);
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
|
|
@ -125,6 +219,57 @@ describe('POST /user/auth/social', () => {
|
|||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.newUser).to.be.false;
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
});
|
||||
|
||||
it('logs an existing user in if they have local auth with matching email', async () => {
|
||||
passport._strategies.google.userProfile.restore();
|
||||
const expectedResult = {
|
||||
id: randomGoogleId,
|
||||
displayName: 'a google user',
|
||||
emails: [
|
||||
{ value: user.auth.local.email },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
expect(response.id).to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('logs an existing user into their social account if they have local auth with matching email', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
// This is important for existing accounts before the new social handling
|
||||
passport._strategies.google.userProfile.restore();
|
||||
const expectedResult = {
|
||||
id: randomGoogleId,
|
||||
displayName: 'a google user',
|
||||
emails: [
|
||||
{ value: user.auth.local.email },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.apiToken).not.to.eql(user.apiToken);
|
||||
expect(response.id).not.to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
|
|
@ -133,11 +278,28 @@ describe('POST /user/auth/social', () => {
|
|||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
expect(response.id).to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('does not log into other account if social auth already exists', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
|
||||
await expect(user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('socialAlreadyExists'),
|
||||
});
|
||||
});
|
||||
|
||||
xit('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@
|
|||
:key="network.key"
|
||||
>
|
||||
<button
|
||||
v-if="!user.auth[network.key].id"
|
||||
v-if="!user.auth[network.key].id && network.key !== 'facebook'"
|
||||
class="btn btn-primary mb-2"
|
||||
@click="socialAuth(network.key, user)"
|
||||
>
|
||||
|
|
@ -387,7 +387,9 @@
|
|||
{{ $t('saveAndConfirm') }}
|
||||
</button>
|
||||
</div>
|
||||
<h5>
|
||||
<h5
|
||||
v-if="user.auth.local.email"
|
||||
>
|
||||
{{ $t('changeEmail') }}
|
||||
</h5>
|
||||
<div
|
||||
|
|
@ -497,25 +499,20 @@
|
|||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
input {
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.usersettings h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.iconalert > div > span {
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.iconalert > div:after {
|
||||
clear: both;
|
||||
content: '';
|
||||
display: table;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: $red-50;
|
||||
font-size: 90%;
|
||||
|
|
@ -768,12 +765,10 @@ export default {
|
|||
if (network === 'apple') {
|
||||
window.location.href = buildAppleAuthUrl();
|
||||
} else {
|
||||
const auth = await hello(network).login({ scope: 'email', options: { force: true } });
|
||||
|
||||
const auth = await hello(network).login({ scope: 'email' });
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
window.location.href = '/';
|
||||
}
|
||||
},
|
||||
|
|
@ -791,8 +786,7 @@ export default {
|
|||
this.localAuth.email = this.user.auth.local.email;
|
||||
}
|
||||
await axios.post('/api/v4/user/auth/local/register', this.localAuth);
|
||||
window.alert(this.$t('addedLocalAuth')); // eslint-disable-line no-alert
|
||||
window.location.href = '/';
|
||||
window.location.href = '/user/settings/site';
|
||||
},
|
||||
restoreEmptyUsername () {
|
||||
if (this.usernameUpdates.username.length < 1) {
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@
|
|||
"unsupportedNetwork": "This network is not currently supported.",
|
||||
"cantDetachSocial": "Account lacks another authentication method; can't detach this authentication method.",
|
||||
"onlySocialAttachLocal": "Local authentication can be added to only a social account.",
|
||||
"socialAlreadyExists": "This social login is already linked to an existing Habitica account.",
|
||||
"invalidReqParams": "Invalid request parameters.",
|
||||
"memberIdRequired": "\"member\" must be a valid UUID.",
|
||||
"heroIdRequired": "\"heroId\" must be a valid UUID.",
|
||||
|
|
|
|||
|
|
@ -643,8 +643,8 @@
|
|||
"backgroundCrypticCandlesText": "Velas crípticas",
|
||||
"backgroundCrypticCandlesNotes": "Invoca fuerzas arcanas entre estas velas crípticas.",
|
||||
"backgroundHauntedPhotoText": "Foto encantada",
|
||||
"backgroundUndeadHandsText": "Mano no-muertas",
|
||||
"backgroundUndeadHandsNotes": "Intenta escapar de las garras de estas manos no-muertas.",
|
||||
"backgroundUndeadHandsText": "Manos muertas vivientes",
|
||||
"backgroundUndeadHandsNotes": "Intenta escapar de las garras de estas Manos muertas vivientes.",
|
||||
"backgrounds102021": "89.ª Serie: publicada en octubre de 2021",
|
||||
"backgroundHauntedPhotoNotes": "Te ves atrapado en el monocromático mundo de una foto encantada.",
|
||||
"backgroundIcePalaceNotes": "Reina desde el Palacio de Hielo.",
|
||||
|
|
@ -684,5 +684,9 @@
|
|||
"backgroundFlowerShopNotes": "Disfruta el suave aroma de una Tienda de Flores.",
|
||||
"backgroundBlossomingTreesNotes": "Entretente bajo Árboles Florecidos.",
|
||||
"backgroundAnimalsDenText": "Cubil de una Criatura del Bosque",
|
||||
"backgroundAnimalsDenNotes": "Ponte Cómodo en el Cubil de una Criatura del Bosque."
|
||||
"backgroundAnimalsDenNotes": "Ponte Cómodo en el Cubil de una Criatura del Bosque.",
|
||||
"backgroundFloweringPrairieNotes": "Brinca por una pradera floreciente.",
|
||||
"backgroundFloweringPrairieText": "Pradera floreciente",
|
||||
"backgroundSpringtimeLakeText": "Lago de Primavera",
|
||||
"backgroundSpringtimeLakeNotes": "Disfruta las vistas a orillas de un Lago de Primavera."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,5 +370,6 @@
|
|||
"hatchingPotionSunset": "Atardecer",
|
||||
"hatchingPotionMoonglow": "Brillolunar",
|
||||
"hatchingPotionSolarSystem": "Sistema solar",
|
||||
"hatchingPotionOnyx": "Ónice"
|
||||
"hatchingPotionOnyx": "Ónice",
|
||||
"hatchingPotionVirtualPet": "Mascota virtual"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -639,5 +639,14 @@
|
|||
"backgroundAutumnPoplarsText": "Bosque de álamos otoñales",
|
||||
"backgroundAutumnLakeshoreText": "Orilla de lago otoñal",
|
||||
"backgroundAutumnLakeshoreNotes": "Párate un momento en la orilla de un lago otoñal para apreciar el reflejo del bosque en sus aguas.",
|
||||
"backgroundAutumnPoplarsNotes": "Regocíjate en los brillantes tonos marrones y dorados de un bosque de álamos otoñales."
|
||||
"backgroundAutumnPoplarsNotes": "Regocíjate en los brillantes tonos marrones y dorados de un bosque de álamos otoñales.",
|
||||
"backgroundFloweringPrairieText": "Pradera floreciente",
|
||||
"backgrounds112021": "CONJUNTO 90: Lanzado en Noviembre 2021",
|
||||
"backgroundCrypticCandlesNotes": "Invoca fuerzas arcanas entre estas velas crípticas.",
|
||||
"backgroundHauntedPhotoText": "Foto encantada",
|
||||
"backgrounds102021": "CONJUNTO 89: Lanzado en Octubre 2021",
|
||||
"backgroundCrypticCandlesText": "Velas crípticas",
|
||||
"backgroundHauntedPhotoNotes": "Te ves atrapado en el mundo monocromático de una foto encantada.",
|
||||
"backgroundUndeadHandsText": "Manos muertas vivientes",
|
||||
"backgroundUndeadHandsNotes": "Intenta escapar de las garras de estas Manos muertas vivientes."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,5 +370,6 @@
|
|||
"hatchingPotionSunset": "Atardecer",
|
||||
"hatchingPotionSolarSystem": "Sistema Solar",
|
||||
"hatchingPotionMoonglow": "Brillo de Luna",
|
||||
"hatchingPotionOnyx": "Ónice"
|
||||
"hatchingPotionOnyx": "Ónice",
|
||||
"hatchingPotionVirtualPet": "Mascota virtual"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,5 +370,6 @@
|
|||
"hatchingPotionSunset": "couché de soleil",
|
||||
"hatchingPotionMoonglow": "lune de miel",
|
||||
"hatchingPotionSolarSystem": "système solaire",
|
||||
"hatchingPotionOnyx": "onyx"
|
||||
"hatchingPotionOnyx": "onyx",
|
||||
"hatchingPotionVirtualPet": "Familier virtuel"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,5 +370,6 @@
|
|||
"hatchingPotionSunset": "Tramonto",
|
||||
"hatchingPotionMoonglow": "Luce di Luna",
|
||||
"hatchingPotionSolarSystem": "Sistema Solare",
|
||||
"hatchingPotionOnyx": "Onice"
|
||||
"hatchingPotionOnyx": "Onice",
|
||||
"hatchingPotionVirtualPet": "Animaletto virtuale"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -744,5 +744,14 @@
|
|||
"questOnyxCollectLeoRunes": "Rune del Leone",
|
||||
"questOnyxCollectOnyxStones": "Pietre d'Onice",
|
||||
"questOnyxDropOnyxPotion": "Pozione di Schiusa Onice",
|
||||
"questOnyxUnlockText": "Sblocca l'acquisto delle Pozioni di Schiusa Onice nel negozio"
|
||||
"questOnyxUnlockText": "Sblocca l'acquisto delle Pozioni di Schiusa Onice nel negozio",
|
||||
"questVirtualPetText": "Caos Virtuale con il pesce d'aprile: L'accensione",
|
||||
"questVirtualPetBoss": "Wotchimon",
|
||||
"questVirtualPetRageTitle": "L'accensione",
|
||||
"questVirtualPetRageDescription": "Questa barra si riempie quando non completi le tue Attività giornaliere. Quando è piena, il Wotchiman curerà il 30% della sua salute rimanente!",
|
||||
"questVirtualPetRageEffect": "`Wotchimon usa il Bip Fastidioso!` Wotchimon emette un bip fastidioso e la sua barra della felicità scompare improvvisamente! I danni in sospeso vengono ridotti.",
|
||||
"questVirtualPetDropVirtualPetPotion": "Pozione di Schiusa Virtuale",
|
||||
"questVirtualPetUnlockText": "Sblocca le Pozioni di Schiusa virtuali nel Negozio",
|
||||
"questVirtualPetNotes": "È una tranquilla e piacevole mattina di primavera ad Habitica, una settimana dopo un memorabile pesce d'aprile. Tu e @Beffymaroo siete presso le stalle a occuparvi dei vostri animali che sono ancora un po' confusi dal loro trascorso virtuale!).<br><br>In lontananza sentite un rimbombo ed una serie di bip, inizialmente deboli ma poi sempre più forti, come se si stessero avvicinando. Un uovo appare all'orizzonte e mentre si avvicina, emettendo dei bip sempre più forti, vedete che è un gigantesco animale domestico virtuale!<br><br>\"Oh no\", esclama @Beffymaroo, \"Penso che il pesce d'aprile gli abbia lasciato ancora qualcosa in sospeso, sembra cercare la nostra attenzione!<br><br>L'animale virtuale emette un bip rabbioso, entra in una furia virtuale ed inizia ad avvicinarsi sempre di più.",
|
||||
"questVirtualPetCompletion": "Un'attenta pressione dei pulsanti sembra aver soddisfatto i misteriosi bisogni dell'animale virtuale, ed alla fine si è calmato e sembrebbe soddisfatto.<br><br>Improvvisamente, in uno scoppio di coriandoli, il Pesce d'Aprile appare con un cesto pieno di strane pozioni che emettono dei flebili bip.<br><br>\"Che tempismo, Pesce d'Aprile\", dice @Beffymaroo con un sorriso ironico. \"Sospetto che questo grosso tizio che suona sia un tuo conoscente.\"<br><br>\"Uh, sì\", dice il Pesce, imbarazzato. “Mi dispiace tanto per questo, e grazie a entrambi per esservi presi cura di Wotchimon! Prendete queste pozioni in segno di ringraziamento, possono riportare indietro i tuoi animali virtuali ogni volta che vuolete!\"<br><br>Non sei sicuro al 100% di voler proprio tutti quei bip, ma sono sicuramente carini quindi vale la pena provare!"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -649,7 +649,7 @@
|
|||
"backgroundCrypticCandlesNotes": "ろうそくの魔方陣で奥義の力を呼び出しましょう。",
|
||||
"backgrounds112021": "セット90:2021年11月リリース",
|
||||
"backgroundFortuneTellersShopNotes": "占い師の店であなたの将来の魅力的なヒントを求めてください。",
|
||||
"backgroundFortuneTellersShopText": "占い師のてん",
|
||||
"backgroundFortuneTellersShopText": "占い師の店",
|
||||
"backgroundInsideAPotionBottleText": "たまごがえしの薬のボトルの中",
|
||||
"backgroundInsideAPotionBottleNotes": "たまごがえしの薬のボトルの中から出たくなって助けを呼びたくなるまで、ガラスをとおして世界をながめましょう。",
|
||||
"backgroundSpiralStaircaseText": "らせん階段",
|
||||
|
|
|
|||
|
|
@ -18,5 +18,37 @@
|
|||
"viewAchievements": "നേട്ടങ്ങൾ കാണിക്കുക",
|
||||
"onboardingComplete": "നിങ്ങൾ നിങ്ങളുടെ ഓൺബോർഡിംഗ് ജോലികൾ പൂർത്തിയാക്കുക!",
|
||||
"onboardingCompleteDescSmall": "നിങ്ങൾക്ക് കൂടുതൽ വേണമെങ്കിൽ, നേട്ടങ്ങൾ പരിശോധിച്ച് ശേഖരിക്കാൻ ആരംഭിക്കുക!",
|
||||
"foundNewItemsExplanation": "ചുമതലകൾ പൂർത്തിയാക്കുന്നത് മുട്ടകൾ, വിരിയിക്കുന്ന മരുന്നുകൾ, വളർത്തുമൃഗങ്ങളുടെ ഭക്ഷണം തുടങ്ങിയ ഇനങ്ങൾ കണ്ടെത്താനുള്ള അവസരം നൽകുന്നു."
|
||||
"foundNewItemsExplanation": "ചുമതലകൾ പൂർത്തിയാക്കുന്നത് മുട്ടകൾ, വിരിയിക്കുന്ന മരുന്നുകൾ, വളർത്തുമൃഗങ്ങളുടെ ഭക്ഷണം തുടങ്ങിയ ഇനങ്ങൾ കണ്ടെത്താനുള്ള അവസരം നൽകുന്നു.",
|
||||
"achievementJustAddWaterText": "നീരാളി, കടൽക്കുതിര, കടൽമത്സ്യം, തിമിംഗലം, ആമ, നുഡിബ്രാഞ്ച്, കടൽ സർപ്പം, ഡോൾഫിൻ എന്നിവയുടെ പെറ്റ് ക്വസ്റ്റുകൾ പൂർത്തിയാക്കി.",
|
||||
"achievementAllYourBaseText": "എല്ലാ അടിസ്ഥാന പർവതങ്ങൾക്കും പേരിട്ടു.",
|
||||
"achievementPurchasedEquipment": "ഒരു ഉപകരണം വാങ്ങുക",
|
||||
"achievementTickledPink": "ഇക്കിളിപ്പെടുത്തിയ പിങ്ക്",
|
||||
"achievementMindOverMatter": "മൈൻഡ് ഓവർ മെറ്റർ",
|
||||
"achievementBackToBasics": "ബേസിക്സിലേക്ക് മടങ്ങുക",
|
||||
"achievementCompletedTaskText": "അവരുടെ ആദ്യ ദൗത്യം പൂർത്തിയാക്കി.",
|
||||
"achievementDustDevil": "പൊടി പിശാച്",
|
||||
"onboardingCompleteDesc": "ലിസ്റ്റ് പൂർത്തിയാക്കിയതിന് നിങ്ങൾ <strong>5 നേട്ടങ്ങളും</strong> <strong class=\"gold-amount\">100 സ്വർണ്ണവും</strong> നേടി.",
|
||||
"achievementLostMasterclasserModalText": "നിങ്ങൾ മാസ്റ്റർക്ലാസ് ക്വസ്റ്റ് സീരീസിലെ പതിനാറ് ക്വസ്റ്റുകളും പൂർത്തിയാക്കി, ലോസ്റ്റ് മാസ്റ്റർക്ലാസിന്റെ രഹസ്യം പരിഹരിച്ചു!",
|
||||
"achievementMindOverMatterText": "റോക്ക്, സ്ലൈം, നൂൽ പെറ്റ് ക്വസ്റ്റുകൾ പൂർത്തിയാക്കി.",
|
||||
"achievementJustAddWater": "വെറും വെള്ളം ചേർക്കുക",
|
||||
"achievementBackToBasicsText": "എല്ലാ അടിസ്ഥാന വളർത്തുമൃഗങ്ങളെയും ശേഖരിച്ചു.",
|
||||
"achievementBackToBasicsModalText": "ആവശ്യമായ എല്ലാ വളർത്തുമൃഗങ്ങളും നിങ്ങൾ ശേഖരിച്ചു!",
|
||||
"achievementAllYourBase": "നിങ്ങളുടെ എല്ലാ അടിസ്ഥാനവും",
|
||||
"achievementAllYourBaseModalText": "നിങ്ങൾ എല്ലാ ബേസ് മൗണ്ടുകളും മെരുക്കി!",
|
||||
"achievementDustDevilText": "മരുഭൂമിയിലെ എല്ലാ വളർത്തുമൃഗങ്ങളെയും ശേഖരിച്ചു.",
|
||||
"achievementDustDevilModalText": "നിങ്ങൾ എല്ലാ മരുഭൂമിയിലെ വളർത്തുമൃഗങ്ങളെയും ശേഖരിച്ചു!",
|
||||
"achievementPartyUp": "നിങ്ങളുടെ ഗ്രൂപ്പിലെ ഒരു അംഗവുമായി നിങ്ങൾ ചേർന്നു!",
|
||||
"achievementAridAuthority": "വരണ്ട അധികാരം",
|
||||
"achievementAridAuthorityText": "എല്ലാ ഡെസേർട്ട് മൗണ്ടുകളും മെരുക്കി.",
|
||||
"achievementAridAuthorityModalText": "നിങ്ങൾ എല്ലാ മരുഭൂമി മലകളെയും മെരുക്കി!",
|
||||
"achievementCompletedTaskModalText": "റിവാർഡുകൾ നേടുന്നതിന് നിങ്ങളുടെ ഏതെങ്കിലും ടാസ്ക്കുകൾ പരിശോധിക്കുക",
|
||||
"achievementHatchedPet": "ഒരു വളർത്തുമൃഗത്തെ വിരിയിക്കുക",
|
||||
"achievementHatchedPetText": "അവരുടെ ആദ്യത്തെ വളർത്തുമൃഗത്തെ വിരിഞ്ഞു.",
|
||||
"achievementHatchedPetModalText": "നിങ്ങളുടെ ഇൻവെന്ററിയിലേക്ക് പോകുക, ഒരു വിരിയുന്ന പോഷനും ഒരു മുട്ടയും സംയോജിപ്പിക്കാൻ ശ്രമിക്കുക",
|
||||
"achievementFedPet": "ഒരു വളർത്തുമൃഗത്തിന് ഭക്ഷണം കൊടുക്കുക",
|
||||
"achievementFedPetText": "അവരുടെ ആദ്യത്തെ വളർത്തുമൃഗത്തിന് ഭക്ഷണം നൽകി.",
|
||||
"achievementFedPetModalText": "പല തരത്തിലുള്ള ഭക്ഷണങ്ങൾ ഉണ്ട്, എന്നാൽ വളർത്തുമൃഗങ്ങൾ തിരഞ്ഞെടുക്കാൻ കഴിയും",
|
||||
"achievementPurchasedEquipmentText": "അവരുടെ ആദ്യത്തെ ഉപകരണം വാങ്ങി.",
|
||||
"achievementPurchasedEquipmentModalText": "നിങ്ങളുടെ അവതാർ ഇഷ്ടാനുസൃതമാക്കാനും സ്ഥിതിവിവരക്കണക്കുകൾ മെച്ചപ്പെടുത്താനുമുള്ള ഒരു മാർഗമാണ് ഉപകരണങ്ങൾ",
|
||||
"achievementTickledPinkText": "എല്ലാ കോട്ടൺ മിഠായി പിങ്ക് വളർത്തുമൃഗങ്ങളും ശേഖരിച്ചു."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -408,5 +408,16 @@
|
|||
"backgroundArchaeologicalDigText": "Archaeological Dig",
|
||||
"backgroundArchaeologicalDigNotes": "Unearth secrets of the ancient past at an Archaeological Dig.",
|
||||
"backgroundScribesWorkshopText": "Scribe's Workshop",
|
||||
"backgroundScribesWorkshopNotes": "Write your next great scroll in a Scribe's Workshop."
|
||||
}
|
||||
"backgroundScribesWorkshopNotes": "Write your next great scroll in a Scribe's Workshop.",
|
||||
"backgroundMedievalKitchenNotes": "ഒരു മധ്യകാല അടുക്കളയിൽ ഒരു കൊടുങ്കാറ്റ് ഉണ്ടാക്കുക.",
|
||||
"backgroundFlowerMarketText": "പൂ വിപണി",
|
||||
"backgroundBirchForestText": "ബിർച്ച് ഫോറസ്റ്റ്",
|
||||
"backgrounds022019": "SET 57: 2019 ഫെബ്രുവരിയിൽ പുറത്തിറങ്ങി",
|
||||
"backgroundMedievalKitchenText": "മധ്യകാല അടുക്കള",
|
||||
"backgroundValentinesDayFeastingHallText": "വാലന്റൈൻസ് ഡേ ഫെസ്റ്റിംഗ് ഹാൾ",
|
||||
"backgroundFlowerMarketNotes": "ഒരു ഫ്ലവർ മാർക്കറ്റിൽ പൂച്ചെണ്ടുകൾക്കോ ഗാർട്ടറിനോ അനുയോജ്യമായ നിറങ്ങൾ കണ്ടെത്തുക.",
|
||||
"backgroundOldFashionedBakeryNotes": "പഴയ രീതിയിലുള്ള ബേക്കറിക്ക് പുറത്ത് രുചികരമായ മണം ആസ്വദിക്കൂ.",
|
||||
"backgrounds042019": "SET 59: 2019 ഏപ്രിലിൽ പുറത്തിറങ്ങി",
|
||||
"backgroundBirchForestNotes": "ശാന്തമായ ബിർച്ച് വനത്തിൽ ഡാലി.",
|
||||
"backgroundOldFashionedBakeryText": "പഴയ രീതിയിലുള്ള ബേക്കറി"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -744,5 +744,13 @@
|
|||
"questOnyxNotes": "@Vikte, @aspiring_advocate e @starsystemic sabem da falta de motivação que tomou conta de você nos últimos tempos e decidem que um dia de diversão pode ajudar a melhorar o seu humor. No entanto, “diversão”, para eles, aparentemente significa mergulhar em alto mar para procurar por tesouros na Fenda Sombria! Vocês vestem seus equipamentos de mergulho, embarcam no bote e remam em direção à antiga cidade de Lentópolis. Durante a viagem, você aproveita para perguntar ao grupo que tipo de tesouro, exatamente, vocês estão procurando.<br><br>“Runas de Plutão!”, responde @Vikte.<br><br>“Não, Runas de Leão!”, exclama @aspiring_advocate.<br><br>“Não, pedras ônix!”, retruca @starsystemic.<br><br>Enquanto eles discutem entre si, você olha para o oceano e avista a entrada da caverna logo abaixo de vocês! Com animação, você dá um salto e mergulha no mar, deixando para trás o trio, que assiste de camarote enquanto você nada em direção à Fenda Sombria para procurar os tesouros ali escondidos.",
|
||||
"questOnyxUnlockText": "Desbloqueia Poções de Eclosão Ônix para compra no Mercado",
|
||||
"questOnyxCollectOnyxStones": "Pedras Ônix",
|
||||
"questOnyxDropOnyxPotion": "Poção Mágica de Eclosão Ônix"
|
||||
"questOnyxDropOnyxPotion": "Poção Mágica de Eclosão Ônix",
|
||||
"questVirtualPetRageTitle": "O Apitamento",
|
||||
"questVirtualPetUnlockText": "Desbloqueia Poções de Eclosão de Mascote Virtual para compra no Mercado",
|
||||
"questVirtualPetRageDescription": "Essa barra enche quando você não completa suas Diárias. Quando estiver completamente cheia, o Gotchimonstro irá curar 30% de sua vida restante!",
|
||||
"questVirtualPetRageEffect": "`O Gotchimonstro usa Apito Irritante!` Gotchimonstro emite um bipe irritante e sua barrinha de felicidade desaparece de repente! Dano pendente reduzido.",
|
||||
"questVirtualPetDropVirtualPetPotion": "Poção de Eclosão de Mascote Virtual",
|
||||
"questVirtualPetBoss": "Gotchimonstro",
|
||||
"questVirtualPetText": "Caos Virtual depois do Primeiro de Abril: O apitamento",
|
||||
"questVirtualPetNotes": "É uma calma e prazerosa manhã de primavera em Habitica, uma semana após um memorável Primeiro de Abril. Você e @Beffymaroo estão no estábulo cuidando de seus mascotes (que ainda estão um pouco confusos com o tempo gasto virtualmente!).<br><br>Você ouve um estrondo longínquo e um som de bipe, suave no início mas que fica cada vez mais alto, como se estivesse se aproximando. Uma forma oval aparece no horizonte e ao se aproximar, apitando cada vez mais alto, você vê que é um bichinho virtual gigantesco!<br><br>\"Ah, não!\" @Beffymaroo exclama, \"Eu acho que o Primeiro de Abril deixou alguns assuntos inacabados com o grandalhão aqui, parece que ele quer atenção!\"<br><br>O bichinho virtual apita nervosamente, fazendo uma birra virtual e retumbando cada vez mais perto."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,5 +189,8 @@
|
|||
"mentioning": "Mencionando",
|
||||
"bannedWordUsedInProfile": "O seu Nome de Exibição ou texto na seção Sobre continha linguagem inapropriada.",
|
||||
"onlyPrivateSpaces": "Somente em espaços privados",
|
||||
"bannedSlurUsedInProfile": "Seu Nome de Exibição ou texto na seção Sobre continha ofensa. Seus privilégios de bate-papo foram revogados."
|
||||
"bannedSlurUsedInProfile": "Seu Nome de Exibição ou texto na seção Sobre continha ofensa. Seus privilégios de bate-papo foram revogados.",
|
||||
"addPasswordAuth": "Adicionar senha",
|
||||
"gemCap": "Limite de Gemas",
|
||||
"nextHourglass": "Próxima Ampulheta Mística"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
{}
|
||||
{
|
||||
"FAQ": "Mga Madalás Naitátanong",
|
||||
"chores": "Mga Gawain"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"noItemsAvailableForType": "You have no <%= type %>.",
|
||||
"foodItemType": "Food",
|
||||
"eggsItemType": "Eggs",
|
||||
"hatchingPotionsItemType": "Hatching Potions",
|
||||
"specialItemType": "Special items",
|
||||
"lockedItem": "Locked Item"
|
||||
"noItemsAvailableForType": "Walá ka ng (mga) <%= type %>.",
|
||||
"foodItemType": "Pagkain para sa Alaga",
|
||||
"eggsItemType": "Itlóg",
|
||||
"hatchingPotionsItemType": "Hatching Potions",
|
||||
"specialItemType": "Mga Pambihirang Bagay",
|
||||
"lockedItem": "Nakakandadong Bagay",
|
||||
"allItems": "Lahát ng Kagamitán"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"settings": "Mga Seting",
|
||||
"language": "Lengguwahe",
|
||||
"americanEnglishGovern": "Sa pagkakataon ng pagkakaiba sa mga pagsasalin ang American English ang susundin.",
|
||||
"helpWithTranslation": "Nais mo bang tumulang sa pagsasalin ng Habitica? Magaling! Bisitahin <a href=\"/groups/guild/7732f64c-33ee-4cce-873c-fc28f147a6f7\">the Aspiring Linguists Guild</a> na!",
|
||||
"settings": "Mga Setting",
|
||||
"language": "Wikà",
|
||||
"americanEnglishGovern": "Sa mga pagkakátaon na may pagkakaiba ang pagsasalin, pátakarán ng American English ang sundin.",
|
||||
"helpWithTranslation": "Nais mo bang tumulong sa pagsasalin ng Habitica? Hanep! Dalawin ang <a href=\"/groups/guild/7732f64c-33ee-4cce-873c-fc28f147a6f7\">the Aspiring Linguists Guild</a>!",
|
||||
"stickyHeader": "Sticky na header",
|
||||
"newTaskEdit": "Buksan ang mga bagong gawain sa edit.",
|
||||
"dailyDueDefaultView": "Iset ang mga Pang-araw-araw na Gawain sa 'angkop' na tab",
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
"json": "(JSON)",
|
||||
"customDayStart": "Sadyang Simula ng Araw",
|
||||
"sureChangeCustomDayStartTime": "Are you sure you want to change your Custom Day Start time? Your Dailies will next reset the first time you use Habitica after <%= time %>. Make sure you have completed your Dailies before then!",
|
||||
"customDayStartHasChanged": "Your custom day start has changed.",
|
||||
"customDayStartHasChanged": "Nagbago ang sadyang simula ng iyóng araw.",
|
||||
"nextCron": "Ang mga Pang-araw-araw na Gawain ay uulit sa unang beses na gumamit ka ng Habitica pagtapos ng <%= time %>. Siguraduhing natapos mo ang iyong mga Pang-araw-araw na Gawain bago ang oras na ito!",
|
||||
"customDayStartInfo1": "Ang Habitica ay sinusuri at inuulit ang iyong mga Pang-araw-araw na Gawain sa hating-gabi dipende sa iyong sariling time zone. Maaari itong baguhin dito.",
|
||||
"misc": "Iba pa",
|
||||
|
|
@ -176,5 +176,6 @@
|
|||
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
|
||||
"usernameNotVerified": "Please confirm your username.",
|
||||
"changeUsernameDisclaimer": "We will be transitioning login names to unique, public usernames soon. This username will be used for invitations, @mentions in chat, and messaging.",
|
||||
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!"
|
||||
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!",
|
||||
"dayStartAdjustment": "Isaayos ang Simulâ ng Araw"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@
|
|||
"weaponArmoireVermilionArcherBowText": "朱红长弓",
|
||||
"weaponArmoireVermilionArcherBowNotes": "用这把耀人的朱红长弓,你的箭会像一颗流行那样飞射出去!增加<%= str %>点力量。魔法衣橱:朱红长弓套装(1/3)。",
|
||||
"weaponArmoireOgreClubText": "食人魔短棒",
|
||||
"weaponArmoireOgreClubNotes": "这跟短棒是从一个真正的食人魔巢穴中打捞出来的。增加<%= str %>点力量。魔法衣橱:食人魔套装(2/3)。",
|
||||
"weaponArmoireOgreClubNotes": "这根短棒是从一个真正的食人魔巢穴中打捞出来的。增加<%= str %>点力量。魔法衣橱:食人魔套装(2/3)。",
|
||||
"weaponArmoireWoodElfStaffText": "木精灵法杖",
|
||||
"weaponArmoireWoodElfStaffNotes": "用掉落的古树枝干做的,这法杖能让你跟森林居民们交流~增加<%= int %>点智力。魔法衣橱:森林精灵套装(3/3)。",
|
||||
"weaponArmoireWandOfHeartsText": "心之魔杖",
|
||||
|
|
@ -811,7 +811,7 @@
|
|||
"armorArmoireSoftBlueSuitText": "柔软的蓝色套装",
|
||||
"armorArmoireSoftBlueSuitNotes": "蓝色是一种令人心静的颜色。太静心了,有些人甚至穿着这套柔软的衣服就睡着了……zZz。增加<%= int %>点智力和<%= per %>点感知。魔法衣橱:蓝家居服(2/3)。",
|
||||
"armorArmoireSoftGreenSuitText": "柔软的绿色套装",
|
||||
"armorArmoireSoftGreenSuitNotes": "绿色是最让人心清气爽的颜色!很适合放松一下疲劳的双眼……emm,或者干脆小睡一下……增加体质、智力各<%= attrs %>点。魔法衣橱:绿居家服(2/3)。",
|
||||
"armorArmoireSoftGreenSuitNotes": "绿色是最让人心清气爽的颜色!很适合放松一下疲劳的双眼……emm,或者干脆小睡一下……增加体质、智力各<%= attrs %>点。魔法衣橱:绿家居服(2/3)。",
|
||||
"armorArmoireSoftRedSuitText": "柔软的红色套装",
|
||||
"armorArmoireSoftRedSuitNotes": "红色是超级振奋人心的颜色。如果你需要早早起床清醒,这套就是完美的睡衣了……增加<%= int %>点智力和<%= str %>点力量。魔法衣橱:红家居服(2/3)。",
|
||||
"armorArmoireScribesRobeText": "书吏长袍",
|
||||
|
|
@ -2281,10 +2281,10 @@
|
|||
"armorMystery202102Notes": "穿着这条轻快明亮的裙子在宇宙漫游,风格优美。没有属性加成。2021年2月订阅者物品。",
|
||||
"armorMystery202102Text": "迷人舞裙",
|
||||
"armorSpecialBirthday2021Notes": "生日快乐,Habitica!穿上这件奢华的派对长袍,庆祝这美妙的一天吧。没有属性加成。",
|
||||
"armorSpecialBirthday2021Text": "奢华的派对长袍",
|
||||
"armorSpecialBirthday2021Text": "奢华派对长袍",
|
||||
"weaponMystery202102Notes": "这根魔杖中发光的粉色宝石能将快乐和友谊传播到远方!没有属性加成。2021年2月订阅者物品。",
|
||||
"weaponMystery202102Text": "迷人魔杖",
|
||||
"armorArmoireSoftPinkSuitNotes": "粉红色是一种舒缓的颜色。来溜进这套休闲装,在日常工作中享受一点宁静!增加<%= per %>点感知力。魔法衣橱:粉色休闲服套装(2/3)。",
|
||||
"armorArmoireSoftPinkSuitNotes": "粉红色是一种舒缓的颜色。来溜进这套休闲装,在日常工作中享受一点宁静!增加<%= per %>点感知力。魔法衣橱:粉家居服(2/3)。",
|
||||
"shieldArmoireSoftPinkPillowNotes": "明智的战士会为任何远征准备枕头。减轻生活的打击... ...即使在你打盹的时候。力量和体质各增加<%= attrs %>点。魔法衣橱:粉色休闲服套装(3/3)。",
|
||||
"shieldArmoireSoftPinkPillowText": "柔软的粉红色枕头",
|
||||
"headArmoirePinkFloppyHatNotes": "许多咒语被缝进了这个简单的帽子,给它一个完美的粉红色。提高<%= int %>点智力。魔法衣橱:粉色休闲服套装(1/3)。",
|
||||
|
|
@ -2452,7 +2452,7 @@
|
|||
"armorMystery202110Notes": "天鹅绒般的铠甲外表看起来十分柔软,但你将受到坚石一般的保护。没有属性加成。2021年10月订阅者物品。",
|
||||
"armorSpecialFall2021WarriorNotes": "一套令人惊叹的正装,非常适合在午夜渡桥时穿着。增加<%= con %>点体质。2021年秋季限定版装备。",
|
||||
"armorSpecialFall2021MageText": "被黑暗支配的长袍",
|
||||
"armorArmoireSoftBlackSuitText": "柔黑色套装",
|
||||
"armorArmoireSoftBlackSuitText": "柔软的黑色套装",
|
||||
"weaponArmoireSkullLanternText": "头骨提灯",
|
||||
"weaponArmoireSkullLanternNotes": "让它的光芒成为你冒险最黑暗的夜晚的向导。增加<%= int %>点智力。魔法衣橱:独立装备。",
|
||||
"armorSpecialFall2021RogueText": "不幸没有防震的铠甲",
|
||||
|
|
@ -2462,7 +2462,7 @@
|
|||
"headSpecialFall2021HealerText": "召唤师面具",
|
||||
"headSpecialFall2021WarriorText": "无头领带",
|
||||
"headSpecialFall2021WarriorNotes": "失去你的头来佩戴领带和领子让这套正式的西装更加完美。增加<%= str %>点力量。2021年秋季限定版装备。",
|
||||
"armorArmoireSoftBlackSuitNotes": "黑色是神秘的颜色。它会激发最有趣的梦想。体质和感知各增加<%= attrs %>点。魔法衣橱:黑色便服(2/3)。",
|
||||
"armorArmoireSoftBlackSuitNotes": "黑色是神秘的颜色。它会激发最有趣的梦想。体质和感知各增加<%= attrs %>点。魔法衣橱:黑家居服(2/3)。",
|
||||
"headSpecialFall2021MageText": "食脑面具",
|
||||
"headSpecialFall2021RogueNotes": "哇,你被卡住了。现在你注定漫游在地牢的走廊中,寻找与收藏碎片。注——定!2021年秋季限定版装备。",
|
||||
"headSpecialFall2021MageNotes": "嘴巴周围的触手将会抓住猎物并将其美味的思想贴近供您品尝。增加<%= per %>点感知。2021年秋季限定版装备。",
|
||||
|
|
@ -2566,5 +2566,11 @@
|
|||
"headAccessoryMystery202203Text": "无畏蜻蜓羽饰",
|
||||
"headAccessoryMystery202203Notes": "需要特别的加速吗? 这首饰上的小小装饰翅膀可比它们看起来厉害多了!没有属性加成。2022年3月订阅者物品。",
|
||||
"backMystery202203Text": "无畏蜻蜓双翼",
|
||||
"backMystery202203Notes": "带上这双闪闪发光的翅膀,你将比天空中所有的生物都要耀眼。没有属性加成。2022年3月订阅者物品。"
|
||||
"backMystery202203Notes": "带上这双闪闪发光的翅膀,你将比天空中所有的生物都要耀眼。没有属性加成。2022年3月订阅者物品。",
|
||||
"armorArmoireSoftVioletSuitNotes": "紫色是奢华的颜色。完成每日任务后需要美美地放松放松。体质和力量各增加<%=attrs%>点。魔法衣橱:紫家居服(2/3)。",
|
||||
"armorArmoireSoftVioletSuitText": "柔软的紫色套装",
|
||||
"armorSpecialBirthday2022Text": "荒谬派对长袍",
|
||||
"armorSpecialBirthday2022Notes": "生日快乐,Habitica!穿上这件荒谬的派对长袍,庆祝这美妙的一天吧。没有属性加成。",
|
||||
"weaponSpecialSpring2022RogueText": "巨型耳钉",
|
||||
"weaponSpecialSpring2022WarriorNotes": "呀! 我想那风比你想象的要大一些,是吧? 强度增加 <%= str %>. 限量版2022年春运装备。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -744,5 +744,7 @@
|
|||
"questOnyxCollectOnyxStones": "玛瑙石",
|
||||
"questOnyxDropOnyxPotion": "玛瑙孵化药水",
|
||||
"questOnyxUnlockText": "在市场中解锁玛瑙孵化药水以购买",
|
||||
"questOnyxCompletion": "你进入黑暗裂缝。生活在那里的螳螂虾飞快地跑开,它们似乎很害怕你。然而,它们又很快地带着小的彩色球体回来了。你意识到这些是其他人想要的宝物!你把每种类型的宝物都装进口袋里,向虾子们告别。然后回到船边,其他人帮助你上船。<br><br>“你去哪里了?”@Vikte惊呼。作为回应,你向他们展示了你收集的宝物。<br><br>“这些材料可以制作玛瑙魔法孵化药水!”,@aspiring_advocate兴奋地说,你开始往岸上走去。<br><br>“也就是说......我们可以孵化玛瑙宠物!”@starsystemic笑着说。\"我们就说了这会很有趣吧?\"<br><br>你以微笑作为回应,为新宠物的到来感到兴奋,并做好准备完成任务!"
|
||||
"questOnyxCompletion": "你进入黑暗裂缝。生活在那里的螳螂虾飞快地跑开,它们似乎很害怕你。然而,它们又很快地带着小的彩色球体回来了。你意识到这些是其他人想要的宝物!你把每种类型的宝物都装进口袋里,向虾子们告别。然后回到船边,其他人帮助你上船。<br><br>“你去哪里了?”@Vikte惊呼。作为回应,你向他们展示了你收集的宝物。<br><br>“这些材料可以制作玛瑙魔法孵化药水!”,@aspiring_advocate兴奋地说,你开始往岸上走去。<br><br>“也就是说......我们可以孵化玛瑙宠物!”@starsystemic笑着说。\"我们就说了这会很有趣吧?\"<br><br>你以微笑作为回应,为新宠物的到来感到兴奋,并做好准备完成任务!",
|
||||
"questVirtualPetDropVirtualPetPotion": "电子宠物孵化药水",
|
||||
"questVirtualPetUnlockText": "在市场上解锁电子宠物孵化药水以购买"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import passport from 'passport';
|
||||
import common from '../../../common';
|
||||
import { BadRequest } from '../errors';
|
||||
import { BadRequest, NotAuthorized } from '../errors';
|
||||
import logger from '../logger';
|
||||
import {
|
||||
generateUsername,
|
||||
|
|
@ -24,7 +24,7 @@ function _passportProfile (network, accessToken) {
|
|||
}
|
||||
|
||||
export async function loginSocial (req, res) { // eslint-disable-line import/prefer-default-export
|
||||
const existingUser = res.locals.user;
|
||||
let existingUser = res.locals.user;
|
||||
const { network } = req.body;
|
||||
|
||||
const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS
|
||||
|
|
@ -47,37 +47,52 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
|
|||
|
||||
// User already signed up
|
||||
if (user) {
|
||||
if (existingUser) {
|
||||
throw new NotAuthorized(res.t('socialAlreadyExists'));
|
||||
}
|
||||
return loginRes(user, req, res);
|
||||
}
|
||||
|
||||
const generatedUsername = generateUsername();
|
||||
let email;
|
||||
if (profile.emails && profile.emails[0] && profile.emails[0].value) {
|
||||
email = profile.emails[0].value.toLowerCase();
|
||||
}
|
||||
|
||||
user = {
|
||||
auth: {
|
||||
[network]: {
|
||||
id: profile.id,
|
||||
emails: profile.emails,
|
||||
},
|
||||
local: {
|
||||
username: generatedUsername,
|
||||
lowerCaseUsername: generatedUsername,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
name: profile.displayName || profile.name || profile.username,
|
||||
},
|
||||
preferences: {
|
||||
language: req.language,
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
};
|
||||
if (!existingUser && email) {
|
||||
existingUser = await User.findOne({ 'auth.local.email': email }).exec();
|
||||
}
|
||||
|
||||
if (existingUser) {
|
||||
existingUser.auth[network] = user.auth[network];
|
||||
existingUser.auth[network] = {
|
||||
id: profile.id,
|
||||
emails: profile.emails,
|
||||
};
|
||||
user = existingUser;
|
||||
} else {
|
||||
const generatedUsername = generateUsername();
|
||||
|
||||
user = {
|
||||
auth: {
|
||||
[network]: {
|
||||
id: profile.id,
|
||||
emails: profile.emails,
|
||||
},
|
||||
local: {
|
||||
username: generatedUsername,
|
||||
lowerCaseUsername: generatedUsername,
|
||||
email,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
name: profile.displayName || profile.name || profile.username,
|
||||
},
|
||||
preferences: {
|
||||
language: req.language,
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
};
|
||||
user = new User(user);
|
||||
user.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
|
||||
}
|
||||
|
|
@ -85,19 +100,15 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
|
|||
const savedUser = await user.save();
|
||||
|
||||
if (!existingUser) {
|
||||
user.newUser = true;
|
||||
savedUser.newUser = true;
|
||||
}
|
||||
|
||||
const response = loginRes(user, req, res);
|
||||
const response = loginRes(savedUser, req, res);
|
||||
|
||||
// Clean previous email preferences
|
||||
if (
|
||||
savedUser.auth[network].emails
|
||||
&& savedUser.auth[network].emails[0]
|
||||
&& savedUser.auth[network].emails[0].value
|
||||
) {
|
||||
if (email) {
|
||||
EmailUnsubscription
|
||||
.remove({ email: savedUser.auth[network].emails[0].value.toLowerCase() })
|
||||
.remove({ email })
|
||||
.exec()
|
||||
.then(() => {
|
||||
if (!existingUser) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue