mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-23 12:43:48 -04:00
Merge branch 'refs/heads/main' into transcember
This commit is contained in:
commit
881259076a
@ -2,6 +2,8 @@
|
||||
tags: ['deploy']
|
||||
needs: []
|
||||
rules:
|
||||
-
|
||||
if: $CI_COMMIT_TAG =~ /^deploy-.*/ && $CI_COMMIT_TAG_MESSAGE =~ $ENVIRONMENT_PATTERN
|
||||
-
|
||||
if: $CI_COMMIT_REF_PROTECTED == 'true' && $DEPLOY_TARGET =~ $ENVIRONMENT_PATTERN
|
||||
-
|
||||
|
@ -47,8 +47,8 @@
|
||||
</template>
|
||||
<template #general>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body d-flex flex-column flex-md-row">
|
||||
<div class="mx-2 text-center">
|
||||
<div class="card-body row">
|
||||
<div class="col-12 col-md-3 text-center">
|
||||
<p v-if="$isGranted('panel') || $isGranted('users') || $isGranted('community')">
|
||||
<nuxt-link to="/admin" class="badge bg-primary text-white">
|
||||
<Icon v="collective-logo.svg" class="inverted" />
|
||||
@ -86,7 +86,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-2 flex-grow-1">
|
||||
<div class="col-12 col-md-9">
|
||||
<Alert type="danger" :message="error" />
|
||||
|
||||
<div v-if="message" class="alert alert-success">
|
||||
@ -126,6 +126,8 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<Alert v-if="$user() && $user()!.email.endsWith('.oauth')" type="warning" message="user.emailMissing" />
|
||||
|
||||
<form :inert="savingEmail" @submit.prevent="changeEmail">
|
||||
<h3 class="h6">
|
||||
<T>user.account.changeEmail.header</T>
|
||||
@ -305,10 +307,9 @@ import type { Profile } from '~/src/profile.ts';
|
||||
export default defineComponent({
|
||||
async setup() {
|
||||
const dialogue = useDialogue();
|
||||
const store = useMainStore();
|
||||
const { accounts, user } = storeToRefs(useMainStore());
|
||||
|
||||
const user = store.user;
|
||||
if (user === null) {
|
||||
if (user.value === null) {
|
||||
throw 'no user';
|
||||
}
|
||||
|
||||
@ -316,7 +317,7 @@ export default defineComponent({
|
||||
const impersonatorCookie = useCookie('impersonator');
|
||||
const termsUpdateDismissed3Cookie = useCookie('termsUpdateDismissed3', longtimeCookieSetting);
|
||||
|
||||
const { data: profilesData } = useFetch(`/api/profile/get/${user.username}?version=2&props=hide`, {
|
||||
const { data: profilesData } = useFetch(`/api/profile/get/${user.value.username}?version=2&props=hide`, {
|
||||
lazy: true,
|
||||
});
|
||||
const { data: socialConnections } = useFetch('/api/user/social-connections', {
|
||||
@ -326,11 +327,11 @@ export default defineComponent({
|
||||
return {
|
||||
config: useConfig(),
|
||||
dialogue,
|
||||
user,
|
||||
username: ref(user.username),
|
||||
email: ref(user.email),
|
||||
socialLookup: ref(user.socialLookup),
|
||||
accounts: store.accounts,
|
||||
user: user.value,
|
||||
username: ref(user.value.username),
|
||||
email: ref(user.value.email),
|
||||
socialLookup: ref(user.value.socialLookup),
|
||||
accounts,
|
||||
tokenCookie,
|
||||
impersonatorCookie,
|
||||
termsUpdateDismissed3Cookie,
|
||||
@ -394,13 +395,13 @@ export default defineComponent({
|
||||
async socialLookup(v) {
|
||||
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/set-social-lookup', { socialLookup: v });
|
||||
|
||||
this.$setToken(response.token);
|
||||
await this.$setToken(response.token);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
const user = await $fetch('/api/user/current');
|
||||
if (user) {
|
||||
this.$setToken(user.token);
|
||||
await this.$setToken(user.token);
|
||||
}
|
||||
|
||||
const redirectTo = window.sessionStorage.getItem('after-login');
|
||||
@ -427,8 +428,8 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
this.$removeToken(this.user.username);
|
||||
this.$setToken(response.token);
|
||||
await this.$removeToken(this.user.username);
|
||||
await this.$setToken(response.token);
|
||||
this.username = this.user.username;
|
||||
this.message = 'crud.saved';
|
||||
this.messageParams = {};
|
||||
@ -472,7 +473,7 @@ export default defineComponent({
|
||||
this.messageParams = {};
|
||||
this.code = null;
|
||||
|
||||
this.$setToken(response.token);
|
||||
await this.$setToken(response.token);
|
||||
this.message = 'crud.saved';
|
||||
this.messageParams = {};
|
||||
this.messageIcon = 'check-circle';
|
||||
@ -490,8 +491,8 @@ export default defineComponent({
|
||||
window.localStorage.removeItem('account-tokens');
|
||||
this.logout();
|
||||
},
|
||||
doLogout() {
|
||||
this.$removeToken();
|
||||
async doLogout() {
|
||||
await this.$removeToken();
|
||||
this.logoutInProgress = false;
|
||||
setTimeout(() => window.location.reload(), 300);
|
||||
},
|
||||
@ -512,13 +513,13 @@ export default defineComponent({
|
||||
async setAvatar(source: string | null) {
|
||||
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/set-avatar', { source });
|
||||
|
||||
this.$setToken(response.token);
|
||||
await this.$setToken(response.token);
|
||||
},
|
||||
async uploaded(ids: string[]) {
|
||||
await this.setAvatar(`${this.$config.public.cloudfront}/images/${ids[0]}-avatar.png`);
|
||||
},
|
||||
async stopImpersonation() {
|
||||
this.$removeToken(this.user.username);
|
||||
await this.$removeToken(this.user.username);
|
||||
this.tokenCookie = this.impersonatorCookie;
|
||||
this.impersonatorCookie = null;
|
||||
setTimeout(() => window.location.reload(), 300);
|
||||
|
@ -53,20 +53,20 @@ export default {
|
||||
'accounts',
|
||||
]),
|
||||
},
|
||||
mounted() {
|
||||
this.$accounts();
|
||||
async mounted() {
|
||||
await this.$accounts();
|
||||
|
||||
// just in case…
|
||||
setTimeout(this.validateAccounts, 1000);
|
||||
setInterval(this.validateAccounts, 60000);
|
||||
},
|
||||
methods: {
|
||||
switchAccount(token) {
|
||||
this.$setToken(token);
|
||||
async switchAccount(token) {
|
||||
await this.$setToken(token);
|
||||
setTimeout(() => window.location.reload(), 300);
|
||||
},
|
||||
addAccount() {
|
||||
this.$setToken(null);
|
||||
async addAccount() {
|
||||
await this.$setToken(null);
|
||||
this.$router.push({ name: 'user' });
|
||||
setTimeout(() => window.location.reload(), 300);
|
||||
},
|
||||
@ -79,11 +79,11 @@ export default {
|
||||
},
|
||||
});
|
||||
if (!user || user.username !== username) {
|
||||
this.$removeToken(username);
|
||||
await this.$removeToken(username);
|
||||
}
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
this.$removeToken(username);
|
||||
await this.$removeToken(username);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,12 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import iconsMetadata from '~/src/iconsMetadata.ts';
|
||||
import type { IconMetadata } from '~/src/iconsMetadata.ts';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
styles?: IconMetadata['styles'];
|
||||
skipIcons?: string[];
|
||||
}>(), {
|
||||
styles: () => ['light'],
|
||||
skipIcons: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits< {
|
||||
change: [icon: string];
|
||||
}>();
|
||||
|
||||
const filter = ref('');
|
||||
const showAll = ref(false);
|
||||
|
||||
const displayLimit = 27;
|
||||
|
||||
const matches = (icon: IconMetadata) => {
|
||||
return !props.skipIcons.includes(icon.name) &&
|
||||
icon.styles.filter((v) => props.styles.includes(v)).length > 0 &&
|
||||
(
|
||||
filter.value === '' ||
|
||||
icon.searchTerms.filter((t) => t.includes(filter.value.toLowerCase())).length > 0
|
||||
)
|
||||
;
|
||||
};
|
||||
|
||||
const visibleIcons = computed(() => {
|
||||
return iconsMetadata.filter(matches).slice(0, showAll.value ? undefined : displayLimit);
|
||||
});
|
||||
|
||||
const filterBar = useTemplateRef('filterBar');
|
||||
onMounted(() => {
|
||||
filterBar.value?.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-light border rounded">
|
||||
<FilterBar ref="filterBar" v-model="filter" />
|
||||
<ul class="list-unstyled icons-list p-2 text-center">
|
||||
<li
|
||||
v-for="icon in visibleIcons"
|
||||
:key="icon.name"
|
||||
class="list-inline-item"
|
||||
>
|
||||
<button class="btn btn-outline-dark border-0 my-2" @click.prevent="$emit('change', icon.name)">
|
||||
<button class="btn btn-outline-dark border-0 my-2" @click.prevent="emit('change', icon.name)">
|
||||
<Icon :v="icon.name" />
|
||||
</button>
|
||||
</li>
|
||||
@ -19,45 +61,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import icons from '../src/icons.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
styles: { default: () => ['light'] },
|
||||
skipIcons: { default: () => [] },
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
icons,
|
||||
filter: '',
|
||||
showAll: false,
|
||||
displayLimit: 27,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visibleIcons() {
|
||||
return this.icons.filter(this.matches).slice(0, this.showAll ? undefined : this.displayLimit);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.filterBar.focus();
|
||||
},
|
||||
methods: {
|
||||
matches(icon) {
|
||||
return !this.skipIcons.includes(icon.name) &&
|
||||
icon.styles.filter((v) => this.styles.includes(v)).length > 0 &&
|
||||
(
|
||||
this.filter === '' ||
|
||||
icon.searchTerms.filter((t) => t.includes(this.filter.toLowerCase())).length > 0
|
||||
)
|
||||
;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icons-list {
|
||||
height: 200px;
|
||||
|
@ -1,9 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import opinions from '../src/opinions.ts';
|
||||
import type { Opinion } from '../src/opinions.ts';
|
||||
import { colours, styles } from '../src/styling.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
readonly?: boolean;
|
||||
maxitems?: number | undefined;
|
||||
}>();
|
||||
|
||||
const modelValue = defineModel<Opinion[]>({ required: true });
|
||||
|
||||
const showIconSelector = ref(false);
|
||||
|
||||
const prototype = () => {
|
||||
return { icon: '', description: '', colour: '', style: '' };
|
||||
};
|
||||
|
||||
const skipIcons = [...Object.values(opinions).map((op) => op.icon), 'ad', 'helicopter', 'meh'];
|
||||
|
||||
const validation = (v: Opinion) => {
|
||||
if (JSON.stringify(v) === JSON.stringify(prototype())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!v.icon) {
|
||||
return 'profile.opinions.validation.missingIcon';
|
||||
}
|
||||
if (!v.description) {
|
||||
return 'profile.opinions.validation.missingDescription';
|
||||
}
|
||||
if (modelValue.value.filter((el) => el.icon === v.icon).length > 1) {
|
||||
return 'profile.opinions.validation.duplicateIcon';
|
||||
}
|
||||
if (modelValue.value.filter((el) => el.description === v.description).length > 1) {
|
||||
return 'profile.opinions.validation.duplicateDescription';
|
||||
}
|
||||
if (v.description.match(/\bkys\b/i) || v.description.match(/\bkill\b/i)) {
|
||||
return 'profile.opinions.validation.kys';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListInput v-model="v" :prototype="prototype()" :group="group" :readonly="readonly" :maxitems="maxitems">
|
||||
<ListInput v-model="modelValue" :prototype="prototype()" :readonly="readonly" :maxitems="maxitems">
|
||||
<template #default="s">
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn', readonly ? 'btn-light border' : 'btn-outline-secondary', showIconSelector === s.i ? 'btn-secondary text-white border' : '']"
|
||||
:class="['btn', props.readonly ? 'btn-light border' : 'btn-outline-secondary', showIconSelector === s.i ? 'btn-secondary text-white border' : '']"
|
||||
:disabled="readonly"
|
||||
@click="showIconSelector = showIconSelector === s.i ? false : s.i"
|
||||
>
|
||||
@ -35,83 +80,21 @@
|
||||
v-if="showIconSelector === s.i"
|
||||
class="hanging shadow shadow-lg border"
|
||||
:skip-icons="skipIcons"
|
||||
@change="s.update({ ...s.val, icon: $event }); showIconSelector = false"
|
||||
@change="(icon) => {
|
||||
s.update({ ...s.val, icon });
|
||||
showIconSelector = false;
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<template #validation="s">
|
||||
<p v-if="validation(s.val)" class="small text-danger">
|
||||
<Icon v="exclamation-triangle" />
|
||||
<span class="ml-1">{{ $t(validation(s.val)) }}</span>
|
||||
<span class="ml-1">{{ $t(validation(s.val)!) }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</ListInput>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import opinions from '../src/opinions.ts';
|
||||
import { colours, styles } from '../src/styling.ts';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
modelValue: {},
|
||||
group: {},
|
||||
readonly: { type: Boolean },
|
||||
maxitems: { default: null, type: Number },
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
v: this.modelValue,
|
||||
showIconSelector: false,
|
||||
colours,
|
||||
styles,
|
||||
skipIcons: [...Object.values(opinions).map((op) => op.icon), 'ad', 'helicopter', 'meh'],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
v() {
|
||||
this.$emit('update:modelValue', this.v);
|
||||
},
|
||||
modelValue(v) {
|
||||
this.v = v;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
prototype() {
|
||||
return { icon: '', description: '', colour: '', style: '' };
|
||||
},
|
||||
setIcon(icon) {
|
||||
this.v.icon = icon;
|
||||
this.showIconSelector = false;
|
||||
this.$emit('update:modelValue', this.v);
|
||||
},
|
||||
validation(v) {
|
||||
if (JSON.stringify(v) === JSON.stringify(this.prototype())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!v.icon) {
|
||||
return 'profile.opinions.validation.missingIcon';
|
||||
}
|
||||
if (!v.description) {
|
||||
return 'profile.opinions.validation.missingDescription';
|
||||
}
|
||||
if (this.v.filter((el) => el.icon === v.icon).length > 1) {
|
||||
return 'profile.opinions.validation.duplicateIcon';
|
||||
}
|
||||
if (this.v.filter((el) => el.description === v.description).length > 1) {
|
||||
return 'profile.opinions.validation.duplicateDescription';
|
||||
}
|
||||
if (v.description.match(/\bkys\b/i) || v.description.match(/\bkill\b/i)) {
|
||||
return 'profile.opinions.validation.kys';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/variables";
|
||||
|
||||
|
@ -1,3 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { importSPKI, jwtVerify } from 'jose';
|
||||
import type { FetchOptions } from 'ofetch';
|
||||
|
||||
import { socialProviders } from '~/src/socialProviders.ts';
|
||||
import type { User } from '~/src/user.ts';
|
||||
|
||||
const { $setToken: setToken } = useNuxtApp();
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
const token = ref<string | null>(null);
|
||||
const usernameOrEmail = ref('');
|
||||
const code = ref('');
|
||||
|
||||
const error = ref('');
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
const captchaToken = ref<string | null>(null);
|
||||
|
||||
const payload = ref<User | null>(null);
|
||||
|
||||
watchEffect(async () => {
|
||||
if (!token.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await setToken(token.value);
|
||||
|
||||
const importedPublicKey = await importSPKI(runtimeConfig.public.publicKey, 'RS256');
|
||||
|
||||
const result = await jwtVerify<User>(token.value, importedPublicKey, {
|
||||
algorithms: ['RS256'],
|
||||
audience: runtimeConfig.public.baseUrl,
|
||||
issuer: runtimeConfig.public.baseUrl,
|
||||
});
|
||||
payload.value = result.payload;
|
||||
});
|
||||
|
||||
const canInit = computed(() => {
|
||||
return usernameOrEmail.value && captchaToken.value;
|
||||
});
|
||||
|
||||
const login = async () => {
|
||||
if (saving.value) {
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await post('/api/user/init', {
|
||||
usernameOrEmail: usernameOrEmail.value,
|
||||
captchaToken: captchaToken.value,
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
const validate = async () => {
|
||||
if (saving.value) {
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await post('/api/user/validate', {
|
||||
code: code.value,
|
||||
}, {
|
||||
headers: {
|
||||
authorization: `Bearer ${token.value}`,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const dialogue = useDialogue();
|
||||
const codeInput = useTemplateRef('codeInput');
|
||||
const post = async (
|
||||
url: string,
|
||||
data: Record<string, unknown>,
|
||||
options: Omit<FetchOptions, 'method' | 'data' | 'timeout'> = {},
|
||||
) => {
|
||||
error.value = '';
|
||||
|
||||
const response = await dialogue.postWithAlertOnError<{ token: string } | { error: string }>(url, data, options);
|
||||
|
||||
usernameOrEmail.value = '';
|
||||
code.value = '';
|
||||
|
||||
if ('error' in response) {
|
||||
error.value = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
token.value = response.token;
|
||||
|
||||
await nextTick();
|
||||
codeInput.value?.focus();
|
||||
};
|
||||
|
||||
const getEmail = (payload: User): string => {
|
||||
return payload.email || payload.emailObfuscated || '';
|
||||
};
|
||||
const addBrackets = (str: string) => {
|
||||
return str ? `(${str})` : '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<Alert type="danger" :message="error" />
|
||||
@ -58,7 +166,7 @@
|
||||
<form :inert="saving" @submit.prevent="validate">
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
ref="code"
|
||||
ref="codeInput"
|
||||
v-model="code"
|
||||
type="text"
|
||||
class="form-control text-center"
|
||||
@ -96,108 +204,3 @@
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import useDialogue from '../composables/useDialogue.ts';
|
||||
import { socialProviders } from '../src/socialProviders.ts';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dialogue: useDialogue(),
|
||||
|
||||
token: null,
|
||||
usernameOrEmail: '',
|
||||
code: '',
|
||||
|
||||
error: '',
|
||||
|
||||
socialProviders,
|
||||
|
||||
saving: false,
|
||||
|
||||
captchaToken: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
payload() {
|
||||
if (!this.token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.$setToken(this.token);
|
||||
|
||||
return jwt.verify(this.token, this.$config.public.publicKey, {
|
||||
algorithm: 'RS256',
|
||||
audience: this.$config.public.baseUrl,
|
||||
issuer: this.$config.public.baseUrl,
|
||||
});
|
||||
},
|
||||
canInit() {
|
||||
return this.usernameOrEmail && this.captchaToken;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
try {
|
||||
await this.post('/api/user/init', {
|
||||
usernameOrEmail: this.usernameOrEmail,
|
||||
captchaToken: this.captchaToken,
|
||||
});
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async validate() {
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
try {
|
||||
await this.post('/api/user/validate', {
|
||||
code: this.code,
|
||||
}, {
|
||||
headers: {
|
||||
authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async post(url, data, options = {}) {
|
||||
this.error = '';
|
||||
|
||||
const response = await this.dialogue.postWithAlertOnError(url, data, options);
|
||||
|
||||
this.usernameOrEmail = '';
|
||||
this.code = '';
|
||||
|
||||
if (response.error) {
|
||||
this.error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
this.token = response.token;
|
||||
|
||||
this.$nextTick((_) => {
|
||||
if (this.$refs.code) {
|
||||
this.$refs.code.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
getEmail(payload) {
|
||||
return payload.email || payload.emailObfuscated || '';
|
||||
},
|
||||
addBrackets(str) {
|
||||
return str ? `(${str})` : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -102,7 +102,7 @@ export default {
|
||||
this.error = res.error;
|
||||
return;
|
||||
}
|
||||
this.$setToken(res.token);
|
||||
await this.$setToken(res.token);
|
||||
} finally {
|
||||
this.saving = false;
|
||||
this.code = '';
|
||||
|
@ -685,6 +685,10 @@ user:
|
||||
Under one account you can have one card per language, and those cards are linked through the common @username.
|
||||
But you can might also want to set up multiple independent accounts, for example one for a work email footer,
|
||||
and a separate one for close friends. If you add them here, you'll be able to quickly switch between those accounts.
|
||||
emailMissing: >
|
||||
Your account was created using a method that didn't share a verified email address with us.
|
||||
Please change the placeholder below to your email and confirm it with a code –
|
||||
this way you'll have a fallback login method in case you lose access to the one you used.
|
||||
|
||||
profile:
|
||||
description: 'Description'
|
||||
|
@ -888,6 +888,10 @@ user:
|
||||
Under one account you can have one card per language, and those cards are linked through the common @username.
|
||||
But you can might also want to set up multiple independent accounts, for example one for a work email footer,
|
||||
and a separate one for close friends. If you add them here, you'll be able to quickly switch between those accounts.
|
||||
emailMissing: >
|
||||
Your account was created using a method that didn't share a verified email address with us.
|
||||
Please change the placeholder below to your email and confirm it with a code –
|
||||
this way you'll have a fallback login method in case you lose access to the one you used.
|
||||
|
||||
profile:
|
||||
description: 'Description'
|
||||
|
@ -99,17 +99,24 @@ const grammarTables: GrammarTable[] = [
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
name: 'come “il”',
|
||||
name: 'seguito da vocale o h muta',
|
||||
morphemeCells: [
|
||||
'article_v',
|
||||
'plural_article_vs',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'seguito da consonante normale',
|
||||
morphemeCells: [
|
||||
'article',
|
||||
'plural_article',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'come “lo”',
|
||||
name: 'seguito da consonante speciale',
|
||||
morphemeCells: [
|
||||
'article_v',
|
||||
'plural_article_v',
|
||||
'article_s',
|
||||
'plural_article_vs',
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -121,51 +128,71 @@ const grammarTables: GrammarTable[] = [
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
name: 'seguito da consonante',
|
||||
name: 'seguito da vocale o h muta',
|
||||
morphemeCells: [
|
||||
'indefinite_article_v',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'seguito da consonante normale',
|
||||
morphemeCells: [
|
||||
'indefinite_article',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'seguito da vocale o h muta',
|
||||
name: 'seguito da consonante speciale',
|
||||
morphemeCells: [
|
||||
'indefinite_article_v',
|
||||
'indefinite_article_s',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: {
|
||||
name: 'Preposizione',
|
||||
short: 'Preposizione',
|
||||
name: 'Preposizione articolata',
|
||||
short: 'Preposizione articolata',
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
name: 'come “del”',
|
||||
morphemeCells: [
|
||||
{ morpheme: 'articled_preposition', prepend: 'de' },
|
||||
{ morpheme: 'plural_articled_prep', prepend: 'de' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'come “dello”',
|
||||
name: '“di” seguito da vocale o h muta',
|
||||
morphemeCells: [
|
||||
{ morpheme: 'articled_preposition_v', prepend: 'de' },
|
||||
{ morpheme: 'plural_articled_prep_v', prepend: 'de' },
|
||||
{ morpheme: 'plural_articled_preposition_v', prepend: 'de' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'come “al”',
|
||||
name: '“di” seguito da consonante normale',
|
||||
morphemeCells: [
|
||||
{ morpheme: 'articled_preposition', prepend: 'a' },
|
||||
{ morpheme: 'plural_articled_prep', prepend: 'a' },
|
||||
{ morpheme: 'articled_preposition', prepend: 'de' },
|
||||
{ morpheme: 'plural_articled_preposition', prepend: 'de' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'come “allo”',
|
||||
name: '“di” seguito da consonante speciale',
|
||||
morphemeCells: [
|
||||
{ morpheme: 'articled_preposition_s', prepend: 'de' },
|
||||
{ morpheme: 'plural_articled_preposition_s', prepend: 'de' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '“a” seguito da vocale o h muta',
|
||||
morphemeCells: [
|
||||
{ morpheme: 'articled_preposition_v', prepend: 'a' },
|
||||
{ morpheme: 'plural_articled_prep_v', prepend: 'a' },
|
||||
{ morpheme: 'plural_articled_preposition_v', prepend: 'a' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '“a” seguito da consonante normale',
|
||||
morphemeCells: [
|
||||
{ morpheme: 'articled_preposition', prepend: 'a' },
|
||||
{ morpheme: 'plural_articled_preposition', prepend: 'a' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '“a” seguito da consonante speciale',
|
||||
morphemeCells: [
|
||||
{ morpheme: 'articled_preposition_s', prepend: 'a' },
|
||||
{ morpheme: 'plural_articled_preposition_s', prepend: 'a' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -5,11 +5,11 @@ Sono stanc{h}{inflection}, ma molto felice. Siamo stanc{h}{inflection}, ma molto
|
||||
{'article} m{possessive_m} bambin{inflection} ha due anni; {article} t{possessive}? {'plural_article} m{plural_possessive_m} bambin{plural_inflection} hanno due anni; {plural_article} t{plural_possessive}?
|
||||
Sei stat{inflection} a casa? Siete stat{inflection} a casa?
|
||||
Ho visto {indefinite_article} ragazz{inflection} bell{inflection}.
|
||||
Sono {indefinite_article_v}artista. Siamo de{l}{articled_preposition} artist{plural_inflection}.
|
||||
Sono {indefinite_article_v}artista. Siamo de{articled_preposition} artist{plural_inflection}.
|
||||
Ale è andat{inflection} da Andrea e Giò, {plural_article} s{plural_possessive} cugin{plural_inflection}
|
||||
{'pronoun_n} canta a{l}{articled_preposition} s{possessive} amic{h}{inflection}. {'pronoun_n} cantano a{l}{plural_articled_prep} loro amic{h}{plural_inflection}.
|
||||
{'pronoun_n} canta a{articled_preposition} s{possessive} amic{h}{inflection}. {'pronoun_n} cantano a{plural_articled_preposition} loro amic{h}{plural_inflection}.
|
||||
{'pronoun_d} parlai per un'ora intera.
|
||||
{'pronoun_a} vedo.
|
||||
Chiama{pronoun_a} e di{l}{pronoun_d} di venire qui. Chiama{pronoun_a} e di' {pronoun_d} di venire qui.
|
||||
{'article_v} psicanalista da cui vado è molto brav{inflection}. {'plural_article_v} psicanalist{plural_inflection} da cui vado sono molto brav{plural_inflection}.
|
||||
Fiore ha chiesto a{l}{articled_preposition_v} zi{inflection} di Sole di aiutar{pronoun_a}. Fiore e Max hanno chiesto a{l}{plural_articled_prep_v} zi{plural_inflection} di Sole di aiutarl{plural_inflection}.
|
||||
{'article_s} psicanalista da cui vado è molto brav{inflection}. {'plural_article_vs} psicanalist{plural_inflection} da cui vado sono molto brav{plural_inflection}.
|
||||
Fiore ha chiesto a{articled_preposition_s} zi{inflection} di Sole di aiutar{pronoun_a}. Fiore e Max hanno chiesto a{plural_articled_preposition_s} zi{plural_inflection} di Sole di aiutarl{plural_inflection}.
|
||||
|
Can't render this file because it has a wrong number of fields in line 7.
|
@ -5,17 +5,21 @@ export default [
|
||||
'inflection',
|
||||
'article',
|
||||
'article_v',
|
||||
'article_s',
|
||||
'indefinite_article',
|
||||
'indefinite_article_v',
|
||||
'indefinite_article_s',
|
||||
'articled_preposition',
|
||||
'articled_preposition_v',
|
||||
'articled_preposition_s',
|
||||
'possessive',
|
||||
'possessive_m',
|
||||
'plural_inflection',
|
||||
'plural_article',
|
||||
'plural_article_v',
|
||||
'plural_articled_prep',
|
||||
'plural_articled_prep_v',
|
||||
'plural_article_vs',
|
||||
'plural_articled_preposition',
|
||||
'plural_articled_preposition_v',
|
||||
'plural_articled_preposition_s',
|
||||
'plural_possessive',
|
||||
'plural_possessive_m',
|
||||
'l',
|
||||
|
@ -1,25 +1,25 @@
|
||||
key description normative pronoun_n pronoun_d pronoun_a inflection plural_inflection article article_v plural_article plural_article_v indefinite_article indefinite_article_v articled_preposition articled_preposition_v plural_articled_prep plural_articled_prep_v possessive plural_possessive possessive_m plural_possessive_m l h i plural pluralHonorific pronounceable history thirdForm smallForm sourcesInfo
|
||||
lui,lui/gli Maschile TRUE lui gli lo o i il lo i gli uno un l llo i gli uo uoi io iei i FALSE FALSE TRUE
|
||||
lei,lei/le Femminile TRUE lei le la a e la la le le una un’ la la la la ua ue ia ie l i FALSE FALSE TRUE
|
||||
loro,loro/gli Maschile plurale TRUE loro loro li i i i gli i gli degli dei i gli i gli uoi uoi iei iei h TRUE FALSE TRUE
|
||||
loro/le Femminile plurale TRUE loro loro le e e le le le le delle delle le le le le ue ue ie ie l h i TRUE FALSE TRUE
|
||||
ləi,ləi/lɜ Inclusivo con schwa (ləi) FALSE ləi lɜ lə ə ɜ lə lə lɜ lɜ unə un* lə lə lɜ lə uə uɜ iə iɜ l i FALSE FALSE TRUE Forme proposte da {https://www.italianoinclusivo.it=Italiano Inclusivo}
|
||||
lai,lai/lɜ Inclusivo con schwa (lai) FALSE lai lɜ lə ə ɜ lə lə lɜ lɜ unə un* lə lə lɜ lə uə uɜ iə iɜ l i FALSE FALSE TRUE
|
||||
loi,loi/lɜ Inclusivo con schwa (loi) FALSE loi lɜ lə ə ɜ lə lə lɜ lɜ unə un* lə lə lɜ lə uə uɜ iə iɜ l i FALSE FALSE TRUE
|
||||
loro/li/li Inclusivo in -i (loro) FALSE loro li li i i i li i li uni uni li li li li uoi uoi iei iei h TRUE FALSE TRUE
|
||||
loi/li Inclusivo in -i (loi) FALSE loi li li i i i li i li uni uni li li li li uoi uoi iei iei h TRUE FALSE TRUE
|
||||
lai/li Inclusivo in -i (lai) FALSE lai li li i i i li i li uni uni li li li li ui ui ii ii h FALSE FALSE TRUE
|
||||
loro/le/le Inclusivo in -e (loro) FALSE loro le le e e le le le le une une le le le le ue ue ie ie l h i TRUE FALSE TRUE
|
||||
loi/le Inclusivo in -e (loi) FALSE loi le le e e le le le le une une le le le le ue ue ie ie l h i TRUE FALSE TRUE
|
||||
lai/le Inclusivo in -e (lai) FALSE lai le le e e le le le le une une le le le le ue ue ie ie l h i TRUE FALSE TRUE
|
||||
loi/ilu Inclusivo con articolo ilu FALSE loi lu lu u u ilu ilu ilu ilu unu unu lu lu lu lu u u iu iu TRUE FALSE TRUE Forme proposte da {/@andrea=@andrea}
|
||||
loi/lu Inclusivo in -u (loi) FALSE lai lu lu u u lu lu lu lu unu unu lu lu lu lu u u iu iu l TRUE FALSE TRUE
|
||||
lai/lu Inclusivo in -u (lai) FALSE lai lu lu u u lu lu lu lu unu unu lu lu lu lu u u iu iu l TRUE FALSE TRUE
|
||||
loie,loie/lie Inclusivo plurale FALSE loie loro lie ie ie ilie ilie ilie ilie unie unie lie lie lie lie uoie uoie ieie ieie h i TRUE FALSE TRUE Forme proposte da {/@andrea=@andrea}
|
||||
lai/l Forme troncate (lai) FALSE lai l l l l l l un un l l l l u u i i l FALSE FALSE TRUE
|
||||
loi/l Forme troncate (loi) FALSE loi l l l l l l un un l l l l uu u i i l FALSE FALSE TRUE
|
||||
lxi,lxi/lx Forme con la x FALSE lxi lx lx x x lx lx lx lx unx unx lx lx lx lx ux ux ix ix l i FALSE FALSE FALSE
|
||||
l*i,l*i/l* Forme con l'asterisco FALSE l*i l* l* * * l* lx l* l* un* un* l* l* l* l* u* u* i* i* l i FALSE FALSE FALSE
|
||||
l_i,l_i/l_ Forme con il trattino basso FALSE l_i l_ l_ _ _ l_ l_ l_ l_ un_ un_ l_ l_ l_ l_ u_ u_ i_ i_ l i FALSE FALSE FALSE
|
||||
l’i,l’i/l’ Forme con l'apostrofo FALSE l’i l’ l’ ’ ’ l’ l’ l’ l’ un’ un’ l’ l’ l’ l’ u’ u’ i’ i’ l i FALSE FALSE FALSE
|
||||
l@i,l@i/l@ Forme con la chiocciola FALSE l@i l@ l@ @ @ l@ l@ l@ l@ un@ un@ l@ l@ l@ l@ u@ u@ i@ i@ l i FALSE FALSE FALSE
|
||||
key description normative pronoun_n pronoun_d pronoun_a inflection plural_inflection article article_v article_s plural_article plural_article_vs indefinite_article indefinite_article_v indefinite_article_s articled_preposition articled_preposition_v articled_preposition_s plural_articled_preposition plural_articled_preposition_v plural_articled_preposition_s possessive plural_possessive possessive_m plural_possessive_m l h i plural pluralHonorific pronounceable history thirdForm smallForm sourcesInfo
|
||||
lui,lui/gli Maschile TRUE lui gli lo o i il l’ lo i gli un un uno l ll’ llo i gli gli uo uoi io iei i FALSE FALSE TRUE
|
||||
lei,lei/le Femminile TRUE lei le la a e la l’ la le le una un’ una lla ll’ lla lle lle lle ua ue ia ie l i FALSE FALSE TRUE
|
||||
loro,loro/gli Maschile plurale TRUE loro loro li i i i l’ gli i gli degli die degli i ll’ gli i gli gli uoi uoi iei iei h TRUE FALSE TRUE
|
||||
loro/le Femminile plurale TRUE loro loro le e e le l’ le le le delle delle delle lle ll’ lle lle lle lle ue ue ie ie l h i TRUE FALSE TRUE
|
||||
ləi,ləi/lɜ Inclusivo con schwa (ləi) FALSE ləi lɜ lə ə ɜ lə l’ lə lɜ lɜ unə un* unə llə ll’ llə llɜ llɜ llɜ uə uɜ iə iɜ l i FALSE FALSE TRUE Forme proposte da {https://www.italianoinclusivo.it=Italiano Inclusivo}
|
||||
lai,lai/lɜ Inclusivo con schwa (lai) FALSE lai lɜ lə ə ɜ lə l’ lə lɜ lɜ unə un* unə llə ll’ llə llɜ llɜ llɜ uə uɜ iə iɜ l i FALSE FALSE TRUE
|
||||
loi,loi/lɜ Inclusivo con schwa (loi) FALSE loi lɜ lə ə ɜ lə l’ lə lɜ lɜ unə un* unə llə ll’ llə llɜ llɜ llɜ uə uɜ iə iɜ l i FALSE FALSE TRUE
|
||||
loro/li/li Inclusivo in -i (loro) FALSE loro li li i i i l’ li i li uni uni uni lli ll’ lli lli lli lli uoi uoi iei iei h TRUE FALSE TRUE
|
||||
loi/li Inclusivo in -i (loi) FALSE loi li li i i i l’ li i li uni uni uni lli ll’ lli lli lli lli uoi uoi iei iei h TRUE FALSE TRUE
|
||||
lai/li Inclusivo in -i (lai) FALSE lai li li i i i l’ li i li uni uni uni lli ll’ lli lli lli lli ui ui ii ii h FALSE FALSE TRUE
|
||||
loro/le/le Inclusivo in -e (loro) FALSE loro le le e e le l’ le le le une une une lle ll’ lle lle lle lle ue ue ie ie l h i TRUE FALSE TRUE
|
||||
loi/le Inclusivo in -e (loi) FALSE loi le le e e le l’ le le le une une une lle ll’ lle lle lle lle ue ue ie ie l h i TRUE FALSE TRUE
|
||||
lai/le Inclusivo in -e (lai) FALSE lai le le e e le l’ le le le une une une lle ll’ lle lle lle lle ue ue ie ie l h i TRUE FALSE TRUE
|
||||
loi/ilu Inclusivo con articolo ilu FALSE loi lu lu u u ilu l’ ilu ilu ilu unu unu unu llu ll’ llu llu llu llu u u iu iu TRUE FALSE TRUE Forme proposte da {/@andrea=@andrea}
|
||||
loi/lu Inclusivo in -u (loi) FALSE lai lu lu u u lu l’ lu lu lu unu unu unu llu ll’ llu llu llu llu u u iu iu l TRUE FALSE TRUE
|
||||
lai/lu Inclusivo in -u (lai) FALSE lai lu lu u u lu l’ lu lu lu unu unu unu llu ll’ llu llu llu llu u u iu iu l TRUE FALSE TRUE
|
||||
loie,loie/lie Inclusivo plurale FALSE loie loro lie ie ie ilie l’ ilie ilie ilie unie unie unie lie ll’ llie lie lle lie uoie uoie ieie ieie h i TRUE FALSE TRUE Forme proposte da {/@andrea=@andrea}
|
||||
lai/l Forme troncate (lai) FALSE lai l l l l’ l l l un un un l ll’ l l l l u u i i l FALSE FALSE TRUE
|
||||
loi/l Forme troncate (loi) FALSE loi l l l l’ l l l un un un l ll’ l l l l uu u i i l FALSE FALSE TRUE
|
||||
lxi,lxi/lx Forme con la x FALSE lxi lx lx x x lx l’ lx lx lx unx unx unx lx ll’ lx lx lx lx ux ux ix ix l i FALSE FALSE FALSE
|
||||
l*i,l*i/l* Forme con l'asterisco FALSE l*i l* l* * * l* l’ lx l* l* un* un* un* l* ll’ l* l* l* l* u* u* i* i* l i FALSE FALSE FALSE
|
||||
l_i,l_i/l_ Forme con il trattino basso FALSE l_i l_ l_ _ _ l_ l’ l_ l_ l_ un_ un_ un_ l_ ll’ l_ l_ l_ l_ u_ u_ i_ i_ l i FALSE FALSE FALSE
|
||||
l’i,l’i/l’ Forme con l'apostrofo FALSE l’i l’ l’ ’ ’ l’ l’ l’ l’ l’ un’ un’ un’ l’ ll’ l’ l’ l’ l’ u’ u’ i’ i’ l i FALSE FALSE FALSE
|
||||
l@i,l@i/l@ Forme con la chiocciola FALSE l@i l@ l@ @ @ l@ l’ l@ l@ l@ un@ un@ un@ l@ ll’ l@ l@ l@ l@ u@ u@ i@ i@ l i FALSE FALSE FALSE
|
||||
|
|
33
locale/pl/blog/zwiazki-partnerskie-konsultacje.md
Normal file
33
locale/pl/blog/zwiazki-partnerskie-konsultacje.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Stanowisko Kolektywu „Rada Języka Neutralnego” w konsultacjach publicznych rządowego projektu ustawy o związkach partnerskich
|
||||
|
||||
<small>2024-11-13 | [@Tess](/@Tess)</small>
|
||||
|
||||

|
||||
|
||||
_Rządowe konsultacje projektu ustawy o związkach partnerskich trwają do 15 listopada 2024 roku. Zgłaszanie uwag jest możliwe poprzez opcję „[wyślij komentarz do projektu](https://legislacja.rcl.gov.pl/projekt/12390651/komentarz)” na portalu Rządowego Procesu Legislacyjnego lub mejlowo, pod adresem [konsultacje.zwiazki@kprm.gov.pl](mailto:konsultacje.zwiazki@kprm.gov.pl). Poniżej publikujemy stanowisko przesłane przez Kolektyw „Rada Języka Neutralnego”. Wszystkie osoby, które nie wysłały jeszcze uwag, zachęcamy do przesyłania opinii – możecie skopiować nasze stanowisko lub na jego podstawie zredagować własne._
|
||||
|
||||
_Możecie także skorzystać z ułatwień technologicznych, proponowanych m.in. przez [Miłość Nie Wyklucza](https://mnw.org.pl/media/konsultacje-zwiazkow-partnerskich-milosc-nie-wyklucza-zaprasza-do-pisania-do-rzadu) czy [Akcję Demokrację](https://kampania.akcjademokracja.pl/letters/zabierz-glos-w-konsultacjach-publicznych-ustawy-o-zwiazkach-partnerskich)._
|
||||
|
||||
_O kluczowych założeniach ustawy o rejestrowanych związkach partnerskich [piszemy tutaj](/blog/zwiazki-partnerskie-2)._
|
||||
|
||||
---
|
||||
|
||||
Szanowny Panie Premierze, Szanowna Pani Ministro Równości, Szanowni Ministrowie i Ministry, Posłowie i Posłanki,
|
||||
|
||||
jako Kolektyw „Rada Języka Neutralnego” chcemy wyrazić poparcie dla projektu ustawy o rejestrowanych związkach partnerskich z dnia 18 października 2024 r. (nr UD87 w wykazie prac legislacyjnych i programowych Rady Ministrów), a także dla projektu ustawy wprowadzającej ustawę o rejestrowanych związkach partnerskich (nr UD88 w wykazie prac legislacyjnych i programowych Rady Ministrów). Jako osoby niebinarne oraz sojusznicze zajmujące się tematyką niebinarności doceniamy, że ustawa umożliwia zawieranie związków bez względu na płeć osób partnerskich.
|
||||
|
||||
Prawdziwa równość wobec prawa nie będzie możliwa bez wprowadzenia równości małżeńskiej, ale rejestrowane związki partnerskie stanowią niezbędne minimum w respektowaniu praw osób LGBTQ+ w Polsce, jak również do realizowania przez Polskę wyroków Europejskiego Trybunału Praw Człowieka. Co więcej, niezależnie od potrzeby wprowadzenia równości małżeńskiej, w polskim systemie prawnym powinna istnieć forma związków odpowiednich dla osób, które bez względu na płeć nie chcą lub nie mogą zawrzeć małżeństwa.
|
||||
|
||||
Jednocześnie sądzimy, że ustawa o rejestrowanych związkach partnerskich powinna zostać rozszerzona o minimum dwie kwestie.
|
||||
|
||||
Pierwszą jest przysposobienie wewnętrzne. Według raportu „Tęczowe rodziny w Polsce” przez pary jednopłciowe jest wychowywanych około 50 tysięcy dzieci. Choć mała piecza jest formą minimalnego zabezpieczenia ich praw, to nadal nie jest to ochrona równa tej, która przysługuje dzieciom w rodzinach heteronormatywnych. Brak przysposobienia wewnętrznego oznacza dyskryminację dzieci wychowywanych w związkach jednopłciowych. Zdajemy sobie sprawę, że wprowadzenie małej pieczy zamiast przysposobienia wewnętrznego ma być formą kompromisu z konserwatystami, ale kompromisem jest już rezygnacja z adopcji – która odbiera wielu dzieciom szansę na prawdziwy dom i zmusza je do życia w systemie zamiast w kochającej rodzinie. To jednak kwestia teoretyczna. Dzieci wychowywane przez dwóch ojców i dwie matki w Polsce – to część naszej rzeczywistości. Jako społeczeństwo jesteśmy winni tym dzieciom ochronę, zapewnienie bezpieczeństwa i możliwości dorastania w szczęśliwej rodzinie. Mała piecza to najdogodniejsze rozwiązanie – w sytuacji, w której drugi rodzic biologiczny dziecka żyje i zachował pełnię praw rodzicielskich.
|
||||
|
||||
Druga kwestia to uregulowanie dostępu do uroczystej ceremonii zawarcia związku partnerskiego. Brak przepisów normujących tę kwestię otwiera furtkę do dyskryminowania tęczowych par przez konserwatywne osoby kierujące Urzędem Stanu Cywilnego. Brak ceremonii w wyraźny sposób lekceważy powagę wydarzenia oraz konsekwencji prawnych wiążących się z nim. Jest to dyskryminacja symboliczna, ale wciąż dotkliwa, pokazująca, w świetle polskiego prawa są osoby „równe” i „równiejsze”. Osoby o konserwatywnych poglądach, które za wszelką cenę chcą odróżnić się od osób queerowych, mogą bez problemu wziąć ślub kościelny. Urzędy Stanu Cywilnego powinny być otwarte na wszystkich, bez względu na orientację seksualną czy tożsamość płciową.
|
||||
|
||||
Ponadto uważamy, że skoro rząd nalega na utworzenie rejestrowanego związku partnerskiego jako instytucji odrębnej i różnej od małżeństwa, to związki takie powinny być formą zdecydowanie bardziej odważną i nowoczesną niż małżeństwa, a więc powinny umożliwiać sformalizowanie związku przez więcej niż dwie osoby. Osoby w relacjach poliamorycznych są niezauważane przez polski system prawny, a formuła związku innego niż małżeński pozwalałaby na dostrzeżenie ich potrzeb.
|
||||
|
||||
Mamy nadzieję, że rząd naszego kraju wywiąże się z obietnic wyborczych i będzie pracować nad Polską, w której wszystkie osoby będą miały zagwarantowane bezpieczeństwo, równość wobec prawa, godność i prawa człowieka.
|
||||
|
||||
Z poważaniem,
|
||||
Kolektyw „Rada Języka Neutralnego”, prowadzący portal zaimki.pl
|
||||
(w składzie: Anna Tess Gołębiowska-Kmieciak, Tomasz Vos-Malenkowicz, Andrea Vos, Archie Pałka, Tymoteusz Lisowski, Karolina Grenda, Sybil Grzybowski, Szymon Misiek)
|
@ -1532,6 +1532,10 @@ user:
|
||||
W ramach jednego konta możesz stworzyć po jednej wizytówce na język, będą one wtedy ze sobą związane poprzez wspólną @nazwę_użytkownicza.
|
||||
Ale możesz też chcieć założyć niezależne konta, na przykład jedno do stopki służbowego maila,
|
||||
a drugie dla bliskich znajomych. Jeśli dodasz je tutaj, będzie można szybko się między tymi kontami przełączać.
|
||||
emailMissing: >
|
||||
Twoje konto zostało utworzone z użyciem metody logowania, która nie udostępniła nam zweryfikowanego adresu email.
|
||||
Aby mieć dostępny zapasowy sposób logowania, gdybyś straciłx dostęp do obecnego,
|
||||
wpisz swój adres email poniżej i potwierdź go kodem.
|
||||
|
||||
profile:
|
||||
description: 'Opis'
|
||||
|
@ -1,11 +1,9 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
|
||||
import replacePlugin from '@rollup/plugin-replace';
|
||||
import yamlPlugin from '@rollup/plugin-yaml';
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
import { defineNuxtConfig } from 'nuxt/config';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
import type { RouteMeta } from 'vue-router';
|
||||
|
||||
import type { Config } from './locale/config.ts';
|
||||
@ -122,6 +120,9 @@ export default defineNuxtConfig({
|
||||
host: hostname, // listen on any host name
|
||||
port,
|
||||
},
|
||||
features: {
|
||||
inlineStyles: false,
|
||||
},
|
||||
compatibilityDate: '2024-07-06',
|
||||
nitro: {
|
||||
rollupConfig: {
|
||||
@ -146,25 +147,6 @@ export default defineNuxtConfig({
|
||||
sumlPlugin(),
|
||||
tsvPlugin(),
|
||||
yamlPlugin(),
|
||||
nodePolyfills({ include: ['crypto', 'stream', 'util'] }),
|
||||
replacePlugin({
|
||||
preventAssignment: false,
|
||||
sourceMap: true,
|
||||
delimiters: ['', ''],
|
||||
values: Object.fromEntries([
|
||||
// workaround for crypto polyfill not working in production mode
|
||||
// https://github.com/davidmyersdev/vite-plugin-node-polyfills/issues/92#issuecomment-2228168969
|
||||
[`if ((crypto && crypto.getRandomValues) || !process.browser) {
|
||||
exports.randomFill = randomFill
|
||||
exports.randomFillSync = randomFillSync
|
||||
} else {
|
||||
exports.randomFill = oldBrowser
|
||||
exports.randomFillSync = oldBrowser
|
||||
}`,
|
||||
`exports.randomFill = randomFill
|
||||
exports.randomFillSync = randomFillSync`],
|
||||
]),
|
||||
}),
|
||||
sentryVitePlugin({
|
||||
disable: !process.env.SENTRY_AUTH_TOKEN,
|
||||
telemetry: false,
|
||||
@ -186,7 +168,7 @@ exports.randomFillSync = randomFillSync`],
|
||||
include: [
|
||||
// list dependencies which trigger “optimized dependencies changed. reloading”
|
||||
// https://github.com/nuxt/nuxt/discussions/27700
|
||||
'@floating-ui/dom',
|
||||
'@floating-ui/vue',
|
||||
'avris-columnist',
|
||||
'avris-futurus',
|
||||
'avris-sorter',
|
||||
@ -291,8 +273,5 @@ exports.randomFillSync = randomFillSync`],
|
||||
},
|
||||
],
|
||||
},
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -46,11 +46,10 @@
|
||||
"he-date": "^1.2.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"ics": "^3.7.6",
|
||||
"jose": "^5.9.6",
|
||||
"js-base64": "^3.5.2",
|
||||
"js-md5": "^0.7.3",
|
||||
"jsdom": "^24.1.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^1.28.1",
|
||||
"markdown-it": "^14.0.0",
|
||||
"markdown-it-mark": "^4.0.0",
|
||||
@ -101,8 +100,6 @@
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/js-md5": "^0.7.2",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/luxon": "^1.27.1",
|
||||
"@types/markdown-it": "^14.0.1",
|
||||
"@types/multer": "1.4.5",
|
||||
@ -138,8 +135,6 @@
|
||||
"tsconfig-paths": "3.14.2",
|
||||
"typescript": "^5.5.2",
|
||||
"vite": "^5.3.5",
|
||||
"vite-plugin-filter-replace": "^0.1.13",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vitest": "^2.0.3",
|
||||
"vue-meta": "^2.4.0",
|
||||
"vue-tsc": "^2.1.6"
|
||||
|
@ -297,7 +297,7 @@ export default {
|
||||
method: 'POST',
|
||||
body: { frequency: parseInt(this.adminNotifications) },
|
||||
});
|
||||
this.store.setToken(res.token);
|
||||
await this.store.setToken(res.token);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -7,24 +7,34 @@ import { isGrantedForUser, parseUserJwt } from '../src/helpers.ts';
|
||||
import type { Account, User } from '../src/user.ts';
|
||||
import { useMainStore } from '../store/index.ts';
|
||||
|
||||
declare module '#app' {
|
||||
interface NuxtApp {
|
||||
$user(): User | null;
|
||||
$isGranted(area?: string, locale?: string | null): boolean;
|
||||
$accounts(): Promise<void>;
|
||||
$setToken(token: string | null): Promise<void>;
|
||||
$removeToken(username?: string | null): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$user(): User | null;
|
||||
$isGranted(area?: string, locale?: string | null): boolean;
|
||||
$accounts(): void;
|
||||
$setToken(token: string): void;
|
||||
$removeToken(username?: string | null): void;
|
||||
$accounts(): Promise<void>;
|
||||
$setToken(token: string | null): Promise<void>;
|
||||
$removeToken(username?: string | null): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const config = useConfig();
|
||||
const store = useMainStore(nuxtApp.$pinia as Pinia);
|
||||
|
||||
const tokenCookie = useCookie('token', longtimeCookieSetting);
|
||||
if (tokenCookie.value) {
|
||||
store.setToken(tokenCookie.value);
|
||||
await store.setToken(tokenCookie.value);
|
||||
if (!store.token) {
|
||||
tokenCookie.value = null;
|
||||
}
|
||||
@ -38,11 +48,11 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
;
|
||||
};
|
||||
|
||||
const getAccounts = (fallback: string | null = null): Record<string, Account> => {
|
||||
const getAccounts = async (fallback: string | null = null): Promise<Record<string, Account>> => {
|
||||
const tokens = (window.localStorage.getItem('account-tokens') || fallback || '').split('|').filter((x) => !!x);
|
||||
const accounts: Record<string, Account> = {};
|
||||
for (const token of tokens) {
|
||||
const account = parseUserJwt(token, runtimeConfig.public.publicKey, runtimeConfig.public.allLocalesUrls);
|
||||
const account = await parseUserJwt(token, runtimeConfig.public.publicKey, runtimeConfig.public.allLocalesUrls);
|
||||
if (account !== null && account.username && account.authenticated) {
|
||||
accounts[account.username] = { token, account };
|
||||
}
|
||||
@ -55,21 +65,21 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
.join('|'));
|
||||
};
|
||||
|
||||
const accounts = (): void => {
|
||||
saveAccounts(getAccounts(store.token));
|
||||
const accounts = async (): Promise<void> => {
|
||||
saveAccounts(await getAccounts(store.token));
|
||||
};
|
||||
const setToken = (token: string): void => {
|
||||
const accounts = getAccounts();
|
||||
const setToken = async (token: string | null): Promise<void> => {
|
||||
const accounts = await getAccounts();
|
||||
|
||||
const usernameBefore = store.user?.username;
|
||||
|
||||
store.setToken(token);
|
||||
await store.setToken(token);
|
||||
if (token) {
|
||||
const account = parseUserJwt(token, runtimeConfig.public.publicKey, runtimeConfig.public.allLocalesUrls);
|
||||
tokenCookie.value = store.token;
|
||||
const account = await parseUserJwt(token, runtimeConfig.public.publicKey, runtimeConfig.public.allLocalesUrls);
|
||||
if (account !== null && account.username && account.authenticated) {
|
||||
accounts[account.username] = { token, account };
|
||||
}
|
||||
tokenCookie.value = store.token;
|
||||
} else {
|
||||
tokenCookie.value = null;
|
||||
}
|
||||
@ -83,18 +93,18 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
bc.close();
|
||||
}
|
||||
};
|
||||
const removeToken = (username: string | null = null): void => {
|
||||
const accounts = getAccounts();
|
||||
const removeToken = async (username: string | null = null): Promise<void> => {
|
||||
const accounts = await getAccounts();
|
||||
|
||||
if (store.user) {
|
||||
delete accounts[username || store.user.username];
|
||||
}
|
||||
if (!username) {
|
||||
if (Object.keys(accounts).length === 0) {
|
||||
store.setToken(null);
|
||||
await store.setToken(null);
|
||||
tokenCookie.value = null;
|
||||
} else {
|
||||
store.setToken(Object.values(accounts)[0].token);
|
||||
await store.setToken(Object.values(accounts)[0].token);
|
||||
tokenCookie.value = store.token;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export default (): Plugin => {
|
||||
const source = await fs.promises.readFile(id, 'utf8');
|
||||
return {
|
||||
code: `export default ${JSON.stringify(marked(source))}`,
|
||||
map: { mappings: '' },
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -25,6 +25,7 @@ export default (): Plugin => {
|
||||
const source = await fs.promises.readFile(id, 'utf8');
|
||||
return {
|
||||
code: `export default ${JSON.stringify(suml.parse(source), replacer)}`.replace(/"<<<{{{([^}]*)}}}>>>"/g, '$1'),
|
||||
map: { mappings: '' },
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -13,6 +13,7 @@ export default (): Plugin => {
|
||||
const content = Papa.parse(await fs.promises.readFile(id, 'utf-8'), tsvParseConfig).data;
|
||||
return {
|
||||
code: `export default ${JSON.stringify(content)}`,
|
||||
map: { mappings: '' },
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
621
pnpm-lock.yaml
generated
621
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -4,13 +4,13 @@ import type { User } from '../src/user.ts';
|
||||
|
||||
import jwt from './jwt.ts';
|
||||
|
||||
export default ({ cookies, headers }: Request): User | undefined => {
|
||||
export default async ({ cookies, headers }: Request): Promise<User | undefined> => {
|
||||
if (headers.authorization && headers.authorization.startsWith('Bearer ')) {
|
||||
return jwt.validate(headers.authorization.substring(7)) as User | undefined;
|
||||
return await jwt.validate<User>(headers.authorization.substring(7));
|
||||
}
|
||||
|
||||
if (cookies.token && cookies.token !== 'null') {
|
||||
return jwt.validate(cookies.token) as User | undefined;
|
||||
return await jwt.validate<User>(cookies.token);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -59,7 +59,7 @@ const shoot = async (db: Database, mode: 'light' | 'dark'): Promise<void> => {
|
||||
});
|
||||
|
||||
for (const { locale, username } of profiles) {
|
||||
const token = jwt.sign(
|
||||
const token = await jwt.sign(
|
||||
{
|
||||
username: 'example',
|
||||
email: 'example@pronouns.page',
|
||||
|
@ -21,7 +21,7 @@ export interface Post {
|
||||
|
||||
const router = Router();
|
||||
|
||||
const getPosts = defineCachedFunction(async () => {
|
||||
export const getPosts = defineCachedFunction(async () => {
|
||||
const dir = `${rootDir}/data/blog`;
|
||||
const posts: Post[] = [];
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
|
@ -68,7 +68,7 @@ const makePublicPronoun = (pronoun: Pronoun, examples: string[] | undefined): Pu
|
||||
const router = Router();
|
||||
|
||||
router.get('/pronouns', handleErrorAsync(async (req, res) => {
|
||||
const publicPronouns: Record<string, Pronoun> = {};
|
||||
const publicPronouns: Record<string, PublicPronoun> = {};
|
||||
for (const [name, pronoun] of Object.entries(pronouns)) {
|
||||
if (pronoun.hidden) {
|
||||
continue;
|
||||
|
@ -267,7 +267,7 @@ export const issueAuthentication = async (
|
||||
};
|
||||
}
|
||||
|
||||
return jwt.sign(userAuthentication satisfies User);
|
||||
return await jwt.sign(userAuthentication satisfies User);
|
||||
};
|
||||
|
||||
const validateHasMxRecords = async (domain: string): Promise<boolean> => {
|
||||
@ -372,8 +372,8 @@ const reloadUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||
) {
|
||||
const token = await issueAuthentication(req.db, dbUserWithMfa, false);
|
||||
res.cookie('token', token, longtimeCookieSetting);
|
||||
req.rawUser = jwt.validate(token) as User;
|
||||
req.user = req.rawUser;
|
||||
req.rawUser = await jwt.validate<User>(token);
|
||||
req.user = req.rawUser!;
|
||||
}
|
||||
next();
|
||||
};
|
||||
@ -436,8 +436,8 @@ export const loadCurrentUser = async (req: Request, res: Response) => {
|
||||
if (req.query.no_cookie === undefined) {
|
||||
res.cookie('token', token, longtimeCookieSetting);
|
||||
}
|
||||
req.rawUser = jwt.validate(token) as User;
|
||||
req.user = req.rawUser;
|
||||
req.rawUser = await jwt.validate<User>(token);
|
||||
req.user = req.rawUser!;
|
||||
|
||||
return res.json({ ...req.user, token });
|
||||
};
|
||||
@ -522,7 +522,7 @@ router.post('/user/init', handleErrorAsync(async (req, res) => {
|
||||
}
|
||||
|
||||
return res.json({
|
||||
token: jwt.sign(
|
||||
token: await jwt.sign(
|
||||
{
|
||||
...payload,
|
||||
email: isEmail ? payload.email : null,
|
||||
@ -704,6 +704,11 @@ router.get('/user/social-redirect/:provider/:locale', handleErrorAsync(async (re
|
||||
return res.redirect(`/api/connect/${req.params.provider}?${new URLSearchParams(searchParams)}`);
|
||||
}));
|
||||
|
||||
const normaliseExternalId = (id: string): string => id
|
||||
.replace(/@/g, '_')
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(new RegExp('/', 'g'), '_');
|
||||
|
||||
// happens on home
|
||||
router.get('/user/social/:provider', handleErrorAsync(async (req, res) => {
|
||||
if (!req.session.grant || !req.session.grant.response ||
|
||||
@ -735,7 +740,7 @@ router.get('/user/social/:provider', handleErrorAsync(async (req, res) => {
|
||||
: req.user;
|
||||
|
||||
const dbUser = await fetchOrCreateUser(req.db, user || {
|
||||
email: payload.email || `${payload.id}@${req.params.provider}.oauth`,
|
||||
email: payload.email || `${normaliseExternalId(payload.id)}@${req.params.provider}.oauth`,
|
||||
name: payload.name,
|
||||
}, req.params.provider);
|
||||
|
||||
|
@ -115,7 +115,7 @@ export class LazyDatabase implements Database {
|
||||
|
||||
router.use(async function (req, res, next) {
|
||||
try {
|
||||
req.rawUser = authenticate(req);
|
||||
req.rawUser = await authenticate(req);
|
||||
req.user = req.rawUser && req.rawUser.authenticated ? req.rawUser : null;
|
||||
req.isGranted = (area: string = '', locale = global.config.locale): boolean => {
|
||||
return !!req.user && isGrantedForUser(req.user, locale, area);
|
||||
|
@ -1,42 +1,45 @@
|
||||
import fs from 'fs';
|
||||
import type { PathLike } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
import * as Sentry from '@sentry/node';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type { JwtPayload } from 'jsonwebtoken';
|
||||
import { importPKCS8, importSPKI, jwtVerify, SignJWT } from 'jose';
|
||||
import type { KeyLike, JWTPayload } from 'jose';
|
||||
|
||||
import { rootDir } from './paths.ts';
|
||||
|
||||
const __dirname = new URL('.', import.meta.url).pathname;
|
||||
|
||||
class Jwt {
|
||||
private readonly privateKey: Buffer;
|
||||
private readonly publicKey: Buffer;
|
||||
constructor(private privateKey: KeyLike, private publicKey: KeyLike) {}
|
||||
|
||||
constructor(privateKey: fs.PathOrFileDescriptor, publicKey: fs.PathOrFileDescriptor) {
|
||||
this.privateKey = fs.readFileSync(privateKey);
|
||||
this.publicKey = fs.readFileSync(publicKey);
|
||||
static async from(privateKeyPath: PathLike, publicKeyPath: PathLike) {
|
||||
const privateKeyPromise = fs.readFile(privateKeyPath, 'utf-8')
|
||||
.then((privateKeyContent) => importPKCS8(privateKeyContent, 'RS256'));
|
||||
const publicKeyPromise = fs.readFile(publicKeyPath, 'utf-8')
|
||||
.then((publicKeyContent) => importSPKI(publicKeyContent, 'RS256'));
|
||||
const [privateKey, publicKey] = await Promise.all([privateKeyPromise, publicKeyPromise]);
|
||||
return new Jwt(privateKey, publicKey);
|
||||
}
|
||||
|
||||
sign(payload: string | object, expiresIn = '365d'): string {
|
||||
return jwt.sign(payload, this.privateKey, {
|
||||
expiresIn,
|
||||
algorithm: 'RS256',
|
||||
audience: process.env.ALL_LOCALES_URLS!.split(','),
|
||||
issuer: process.env.BASE_URL,
|
||||
});
|
||||
async sign(payload: JWTPayload, expiresIn = '365d'): Promise<string> {
|
||||
return await new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: 'RS256' })
|
||||
.setExpirationTime(expiresIn)
|
||||
.setAudience(process.env.ALL_LOCALES_URLS!.split(','))
|
||||
.setIssuer(process.env.BASE_URL!)
|
||||
.sign(this.privateKey);
|
||||
}
|
||||
|
||||
validate(token: string): JwtPayload | string | undefined {
|
||||
async validate<PayloadType = JWTPayload>(token: string): Promise<PayloadType | undefined> {
|
||||
try {
|
||||
return jwt.verify(token, this.publicKey, {
|
||||
const { payload } = await jwtVerify<PayloadType>(token, this.publicKey, {
|
||||
algorithms: ['RS256'],
|
||||
audience: process.env.ALL_LOCALES_URLS!.split(','),
|
||||
issuer: process.env.ALL_LOCALES_URLS!.split(','),
|
||||
});
|
||||
return payload;
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Jwt(`${rootDir}/keys/private.pem`, `${rootDir}/keys/public.pem`);
|
||||
export default await Jwt.from(`${rootDir}/keys/private.pem`, `${rootDir}/keys/public.pem`);
|
||||
|
@ -1,22 +1,26 @@
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
import { Feed } from 'feed';
|
||||
import { defineNuxtRouteMiddleware, useNuxtApp, useRuntimeConfig } from 'nuxt/app';
|
||||
import marked from 'marked';
|
||||
|
||||
import useConfig from '~/composables/useConfig.ts';
|
||||
import type { Post } from '~/server/express/blog.ts';
|
||||
import type { Translations } from '~/locale/translations.ts';
|
||||
import { getPosts } from '~/server/express/blog.ts';
|
||||
import { loadSuml, loadSumlFromBase } from '~/server/loader.ts';
|
||||
import { rootDir } from '~/server/paths.ts';
|
||||
import parseMarkdown from '~/src/parseMarkdown.ts';
|
||||
import { Translator } from '~/src/translator.ts';
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const { $translator: translator, ssrContext } = useNuxtApp();
|
||||
const res = ssrContext?.event.node.res;
|
||||
const config = global.config;
|
||||
|
||||
if (!res || to.path !== '/blog.atom') {
|
||||
return;
|
||||
}
|
||||
const translations = loadSuml('translations') as Translations;
|
||||
const baseTranslations = loadSumlFromBase('locale/_base/translations') as Translations;
|
||||
|
||||
const config = useConfig();
|
||||
const translator = new Translator(translations, baseTranslations, config);
|
||||
|
||||
export default defineCachedEventHandler(async () => {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
const posts = await $fetch<Post[]>('/api/blog');
|
||||
const posts = await getPosts();
|
||||
|
||||
const feed = new Feed({
|
||||
title: `${translator.translate('title')} • ${translator.translate('links.blog')}`,
|
||||
@ -31,7 +35,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
});
|
||||
|
||||
for (const post of posts) {
|
||||
const markdown = (await import(`../data/blog/${post.slug}.md`)).default;
|
||||
const markdownContent = await fs.readFile(`${rootDir}/data/blog/${post.slug}.md`, 'utf-8');
|
||||
const markdown = marked(markdownContent);
|
||||
const parsed = await parseMarkdown(markdown, translator);
|
||||
|
||||
feed.addItem({
|
||||
@ -49,8 +54,11 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
});
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');
|
||||
res.end(feed.atom1());
|
||||
|
||||
return new Promise(() => {}); // halt execution
|
||||
return new Response(feed.atom1(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/rss+xml; charset=utf-8',
|
||||
},
|
||||
});
|
||||
}, {
|
||||
maxAge: Infinity,
|
||||
});
|
@ -1,11 +1,11 @@
|
||||
import fs from 'fs';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
import type { GrantConfig, GrantResponse } from 'grant';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { importPKCS8, SignJWT } from 'jose';
|
||||
|
||||
import { rootDir } from './paths.ts';
|
||||
|
||||
const getAppleClientSecret = (): string => {
|
||||
const getAppleClientSecret = async (): Promise<string | undefined> => {
|
||||
const headers = {
|
||||
alg: 'ES256',
|
||||
kid: process.env.APPLE_KEY_ID,
|
||||
@ -17,12 +17,18 @@ const getAppleClientSecret = (): string => {
|
||||
sub: process.env.APPLE_CLIENT_ID,
|
||||
};
|
||||
const applePrivateKeyFile = `${rootDir}/keys/AuthKey_${process.env.APPLE_KEY_ID}.p8`;
|
||||
const privateKey = fs.existsSync(applePrivateKeyFile) ? fs.readFileSync(applePrivateKeyFile).toString('utf-8') : '';
|
||||
return jwt.sign(claims, privateKey, {
|
||||
algorithm: 'ES256',
|
||||
header: headers,
|
||||
expiresIn: '180d',
|
||||
});
|
||||
let privateKeyContent;
|
||||
try {
|
||||
privateKeyContent = await fs.readFile(applePrivateKeyFile, 'utf-8');
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
const privateKey = await importPKCS8(privateKeyContent, 'ES256');
|
||||
|
||||
return await new SignJWT(claims)
|
||||
.setProtectedHeader(headers)
|
||||
.setExpirationTime('180d')
|
||||
.sign(privateKey);
|
||||
};
|
||||
|
||||
function appleIsEnabled(): boolean {
|
||||
@ -93,7 +99,7 @@ export const config: GrantConfig = {
|
||||
if (enableApple) {
|
||||
config.apple = {
|
||||
key: process.env.APPLE_CLIENT_ID,
|
||||
secret: getAppleClientSecret(),
|
||||
secret: await getAppleClientSecret(),
|
||||
|
||||
callback: '/api/user/social/apple',
|
||||
scope: ['openid', 'name', 'email'],
|
||||
@ -108,7 +114,7 @@ if (enableApple) {
|
||||
|
||||
interface SocialProfile {
|
||||
id: string;
|
||||
email: string;
|
||||
email: string | null;
|
||||
name: string;
|
||||
username?: string;
|
||||
avatar?: string;
|
||||
@ -168,8 +174,7 @@ export const handlers: Record<string, (r: GrantResponse) => SocialProfile> = {
|
||||
const acct = `${r.profile.username}@${instance}`;
|
||||
return {
|
||||
id: acct,
|
||||
// very possibly not really operated by the user
|
||||
email: acct,
|
||||
email: null,
|
||||
name: acct,
|
||||
avatar: r.profile.avatar,
|
||||
access_token: r.access_token,
|
||||
@ -179,7 +184,7 @@ export const handlers: Record<string, (r: GrantResponse) => SocialProfile> = {
|
||||
indieauth(r: GrantResponse): SocialProfile {
|
||||
return {
|
||||
id: r.profile.me,
|
||||
email: `indieauth@${r.profile.domain}`,
|
||||
email: null,
|
||||
name: r.profile.domain,
|
||||
instance: (r as { instance: string }).instance,
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import { importSPKI, jwtVerify } from 'jose';
|
||||
import { Base64 } from 'js-base64';
|
||||
import md5 from 'js-md5';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import _ from 'lodash';
|
||||
|
||||
import type { Database } from '../server/db.ts';
|
||||
|
||||
@ -154,7 +153,7 @@ export const now = (): number => {
|
||||
};
|
||||
|
||||
export const isEmoji = (char: string): boolean => {
|
||||
return _.toArray(char).length === 1 && !!char.trim().match(/(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/);
|
||||
return !!char.match(/[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff]/);
|
||||
};
|
||||
|
||||
export function zip<K extends keyof unknown, V>(list: [K, V][], reverse: false): Record<K, V>;
|
||||
@ -458,17 +457,19 @@ export const addSlash = (link: string): string => {
|
||||
return link + (['*', '\''].includes(link.substring(link.length - 1)) ? '/' : '');
|
||||
};
|
||||
|
||||
export const parseUserJwt = (token: string, publicKey: string, allLocalesUrls: string[]): User | null => {
|
||||
export const parseUserJwt = async (
|
||||
token: string,
|
||||
publicKey: string,
|
||||
allLocalesUrls: string[],
|
||||
): Promise<User | null> => {
|
||||
try {
|
||||
const parsed = jwt.verify(token, publicKey, {
|
||||
const importedPublicKey = await importSPKI(publicKey, 'RS256');
|
||||
const { payload: user } = await jwtVerify<User>(token, importedPublicKey, {
|
||||
algorithms: ['RS256'],
|
||||
audience: allLocalesUrls,
|
||||
issuer: allLocalesUrls,
|
||||
});
|
||||
if (typeof parsed === 'string') {
|
||||
return null;
|
||||
}
|
||||
return parsed as User;
|
||||
return user;
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
return null;
|
||||
|
15
src/icons.js
15
src/icons.js
@ -1,15 +0,0 @@
|
||||
import iconsMetadata from '@fortawesome/fontawesome-pro/metadata/icons.yml';
|
||||
|
||||
const icons = [];
|
||||
for (const [iconName, iconMetadata] of Object.entries(iconsMetadata)) {
|
||||
icons.push({
|
||||
name: iconName,
|
||||
styles: iconMetadata.styles,
|
||||
searchTerms: [
|
||||
...iconMetadata.search.terms.map((t) => `${t}`.toLowerCase()),
|
||||
iconName.toLowerCase(),
|
||||
iconMetadata.label.toLowerCase(),
|
||||
],
|
||||
});
|
||||
}
|
||||
export default icons;
|
22
src/iconsMetadata.ts
Normal file
22
src/iconsMetadata.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import iconsMetadataRaw from '@fortawesome/fontawesome-pro/metadata/icons.yml';
|
||||
import type { IconStyle } from '@fortawesome/fontawesome-pro/metadata/icons.yml';
|
||||
|
||||
export interface IconMetadata {
|
||||
name: string;
|
||||
styles: IconStyle[];
|
||||
searchTerms: string[];
|
||||
}
|
||||
|
||||
const iconsMetadata: IconMetadata[] = [];
|
||||
for (const [iconName, iconMetadataRaw] of Object.entries(iconsMetadataRaw)) {
|
||||
iconsMetadata.push({
|
||||
name: iconName,
|
||||
styles: iconMetadataRaw.styles,
|
||||
searchTerms: [
|
||||
...iconMetadataRaw.search.terms.map((t) => `${t}`.toLowerCase()),
|
||||
iconName.toLowerCase(),
|
||||
iconMetadataRaw.label.toLowerCase(),
|
||||
],
|
||||
});
|
||||
}
|
||||
export default iconsMetadata;
|
@ -3,6 +3,7 @@ export interface User {
|
||||
username: string;
|
||||
email: string;
|
||||
emailHash?: string;
|
||||
emailObfuscated?: string;
|
||||
roles: string;
|
||||
avatarSource: string;
|
||||
bannedReason: string;
|
||||
|
@ -44,7 +44,7 @@ export const useMainStore = defineStore('main', {
|
||||
runtimeConfig: useRuntimeConfig(),
|
||||
}),
|
||||
actions: {
|
||||
setToken(token: string | null) {
|
||||
async setToken(token: string | null) {
|
||||
if (!token) {
|
||||
this.token = null;
|
||||
this.user = null;
|
||||
@ -52,7 +52,7 @@ export const useMainStore = defineStore('main', {
|
||||
}
|
||||
|
||||
const publicKey = this.runtimeConfig.public.publicKey;
|
||||
const user = parseUserJwt(token, publicKey, this.runtimeConfig.public.allLocalesUrls);
|
||||
const user = await parseUserJwt(token, publicKey, this.runtimeConfig.public.allLocalesUrls);
|
||||
|
||||
if (user && user.mfaRequired) {
|
||||
this.preToken = token;
|
||||
|
13
types/fontawesome.d.ts
vendored
Normal file
13
types/fontawesome.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
declare module '@fortawesome/fontawesome-pro/metadata/icons.yml' {
|
||||
export type IconStyle = 'solid' | 'regular' | 'light' | 'duotone' | 'brands';
|
||||
interface IconMetadata {
|
||||
changes: string[];
|
||||
label: string;
|
||||
search: { terms: string[] };
|
||||
styles: IconStyle[];
|
||||
unicode: string;
|
||||
voted: boolean;
|
||||
}
|
||||
const iconMetadata: Record<string, IconMetadata>;
|
||||
export default iconMetadata;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user