6 Commits

Author SHA1 Message Date
Sonny
70fc82dea8 chore: release v3.3.1 2025-11-09 03:24:03 +01:00
Sonny
3591d032c2 fix: broken imports 2025-11-09 03:23:53 +01:00
Sonny
e4aea038fe chore: release v3.3.0 2025-11-09 03:18:11 +01:00
Sonny
d03004b841 chore(deps): update deps 2025-11-09 03:17:57 +01:00
Sonny
64cd4820c5 docs: add google oauth config step 2025-11-09 03:11:21 +01:00
Sonny
1c81a6a86f feat: allow adding links via ip 2025-11-09 03:10:51 +01:00
18 changed files with 1386 additions and 1026 deletions

View File

@@ -1,6 +1,6 @@
# Source : https://github.com/adonisjs-community/adonis-packages/blob/main/Dockerfile # Source : https://github.com/adonisjs-community/adonis-packages/blob/main/Dockerfile
FROM node:24.6-alpine3.22 AS base FROM node:24.11-alpine3.22 AS base
RUN apk --no-cache add curl RUN apk --no-cache add curl
RUN corepack enable RUN corepack enable

View File

@@ -92,6 +92,27 @@ pnpm run start
openssl rand -base64 32 openssl rand -base64 32
``` ```
### Google OAuth
Pour obtenir le Client ID et Secret Google nécessaires à l'authentification :
1. Accédez à la [Console Google Cloud](https://console.cloud.google.com/)
2. Créez un nouveau projet ou sélectionnez un projet existant
3. Activez l'API **Google+ API** (ou utilisez directement l'API OAuth 2.0)
4. Allez dans **Identifiants** (Credentials) > **Créer des identifiants** > **ID client OAuth 2.0**
5. Configurez l'écran de consentement OAuth si ce n'est pas déjà fait :
- Type d'application : **Interne** ou **Externe**
- Remplissez les informations requises (nom de l'application, email de support, etc.)
6. Créez l'ID client OAuth 2.0 :
- Type d'application : **Application Web**
- Nom : choisissez un nom pour votre application
- URI de redirection autorisés : ajoutez `http://localhost:3333/auth/callback` pour le développement (ou votre URL de production + `/auth/callback`)
7. Une fois créé, vous obtiendrez :
- **Client ID** : à définir dans `GOOGLE_CLIENT_ID`
- **Client Secret** : à définir dans `GOOGLE_CLIENT_SECRET`
> **Note** : Pour la production, assurez-vous d'ajouter votre URL de production dans les URI de redirection autorisés (ex: `https://votre-domaine.com/auth/callback`)
### GitHub Actions ### GitHub Actions
Env var to define : Env var to define :

View File

@@ -0,0 +1,9 @@
import vine from '@vinejs/vine';
export const baseLinkValidator = vine.object({
name: vine.string().trim().minLength(1).maxLength(254),
description: vine.string().trim().maxLength(300).optional(),
url: vine.string().url({ require_tld: false }).trim(),
favorite: vine.boolean(),
collectionId: vine.number(),
});

View File

@@ -1,11 +1,8 @@
import { baseLinkValidator } from '#links/validators/base_link_validator';
import vine from '@vinejs/vine'; import vine from '@vinejs/vine';
export const createLinkValidator = vine.compile( export const createLinkValidator = vine.compile(
vine.object({ vine.object({
name: vine.string().trim().minLength(1).maxLength(254), ...baseLinkValidator.getProperties(),
description: vine.string().trim().maxLength(300).optional(),
url: vine.string().trim(),
favorite: vine.boolean(),
collectionId: vine.number(),
}) })
); );

View File

@@ -1,13 +1,10 @@
import { params } from '#core/validators/params_object'; import { params } from '#core/validators/params_object';
import { baseLinkValidator } from '#links/validators/base_link_validator';
import vine from '@vinejs/vine'; import vine from '@vinejs/vine';
export const updateLinkValidator = vine.compile( export const updateLinkValidator = vine.compile(
vine.object({ vine.object({
name: vine.string().trim().minLength(1).maxLength(254), ...baseLinkValidator.getProperties(),
description: vine.string().trim().maxLength(300).optional(),
url: vine.string().trim(),
favorite: vine.boolean(),
collectionId: vine.number(),
params, params,
}) })

View File

@@ -21,7 +21,7 @@ interface FormCollectionProps extends FormLayoutProps {
handleSubmit: () => void; handleSubmit: () => void;
} }
export default function MantineFormCollection({ export function FormCollection({
data, data,
errors, errors,
disableInputs = false, disableInputs = false,

View File

@@ -24,7 +24,7 @@ interface FormLinkProps extends FormLayoutProps {
handleSubmit: () => void; handleSubmit: () => void;
} }
export default function MantineFormLink({ export function FormLink({
data, data,
errors, errors,
collections, collections,
@@ -83,7 +83,7 @@ export default function MantineFormLink({
value: id.toString(), value: id.toString(),
}))} }))}
onChange={(value) => setData('collectionId', value)} onChange={(value) => setData('collectionId', value)}
value={data.collectionId.toString()} value={data.collectionId?.toString()}
readOnly={disableInputs} readOnly={disableInputs}
mt="md" mt="md"
searchable searchable

View File

@@ -16,15 +16,50 @@ export const appendResourceId = (
) => `${url}${resourceId ? `/${resourceId}` : ''}`; ) => `${url}${resourceId ? `/${resourceId}` : ''}`;
export function isValidHttpUrl(urlParam: string) { export function isValidHttpUrl(urlParam: string) {
let url; const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}(:\d+)?(\/.*)?(\?.*)?(#[^#]*)?$/;
const domainRegex =
/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(:\d+)?(\/.*)?(\?.*)?(#[^#]*)?$/;
const simpleDomainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/;
try { let urlToTest = urlParam.trim();
url = new URL(urlParam);
} catch (_) { if (urlToTest.startsWith('http://') || urlToTest.startsWith('https://')) {
return false; try {
const url = new URL(urlToTest);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch (_) {
return false;
}
} }
return url.protocol === 'http:' || url.protocol === 'https:'; if (ipv4Regex.test(urlToTest)) {
try {
new URL(`http://${urlToTest}`);
return true;
} catch (_) {
return false;
}
}
if (domainRegex.test(urlToTest)) {
try {
new URL(`http://${urlToTest}`);
return true;
} catch (_) {
return false;
}
}
if (simpleDomainRegex.test(urlToTest)) {
try {
new URL(`http://${urlToTest}`);
return true;
} catch (_) {
return false;
}
}
return false;
} }
export const generateShareUrl = ( export const generateShareUrl = (

View File

@@ -2,7 +2,8 @@ import { useForm } from '@inertiajs/react';
import { route } from '@izzyjs/route/client'; import { route } from '@izzyjs/route/client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MantineFormCollection, { import {
FormCollection,
FormCollectionData, FormCollectionData,
} from '~/components/form/form_collection'; } from '~/components/form/form_collection';
import { Visibility } from '~/types/app'; import { Visibility } from '~/types/app';
@@ -29,7 +30,7 @@ export default function CreateCollectionPage({
}; };
return ( return (
<MantineFormCollection <FormCollection
title={t('collection.create')} title={t('collection.create')}
textSubmitButton={t('form.create')} textSubmitButton={t('form.create')}
canSubmit={!isFormDisabled} canSubmit={!isFormDisabled}

View File

@@ -1,7 +1,8 @@
import { useForm } from '@inertiajs/react'; import { useForm } from '@inertiajs/react';
import { route } from '@izzyjs/route/client'; import { route } from '@izzyjs/route/client';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MantineFormCollection, { import {
FormCollection,
FormCollectionData, FormCollectionData,
} from '~/components/form/form_collection'; } from '~/components/form/form_collection';
import { Collection } from '~/types/app'; import { Collection } from '~/types/app';
@@ -27,7 +28,7 @@ export default function DeleteCollectionPage({
}; };
return ( return (
<MantineFormCollection <FormCollection
title={t('collection.delete')} title={t('collection.delete')}
textSubmitButton={t('form.delete')} textSubmitButton={t('form.delete')}
canSubmit={!processing} canSubmit={!processing}

View File

@@ -2,7 +2,8 @@ import { useForm } from '@inertiajs/react';
import { route } from '@izzyjs/route/client'; import { route } from '@izzyjs/route/client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MantineFormCollection, { import {
FormCollection,
FormCollectionData, FormCollectionData,
} from '~/components/form/form_collection'; } from '~/components/form/form_collection';
import { Collection } from '~/types/app'; import { Collection } from '~/types/app';
@@ -38,7 +39,7 @@ export default function EditCollectionPage({
}; };
return ( return (
<MantineFormCollection <FormCollection
title={t('collection.edit')} title={t('collection.edit')}
textSubmitButton={t('form.update')} textSubmitButton={t('form.update')}
canSubmit={canSubmit} canSubmit={canSubmit}

View File

@@ -2,7 +2,7 @@ import { useForm } from '@inertiajs/react';
import { route } from '@izzyjs/route/client'; import { route } from '@izzyjs/route/client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MantineFormLink from '~/components/form/form_link'; import { FormLink } from '~/components/form/form_link';
import useSearchParam from '~/hooks/use_search_param'; import useSearchParam from '~/hooks/use_search_param';
import { isValidHttpUrl } from '~/lib/navigation'; import { isValidHttpUrl } from '~/lib/navigation';
import { Collection } from '~/types/app'; import { Collection } from '~/types/app';
@@ -38,7 +38,7 @@ export default function CreateLinkPage({
}; };
return ( return (
<MantineFormLink <FormLink
title={t('link.create')} title={t('link.create')}
textSubmitButton={t('form.create')} textSubmitButton={t('form.create')}
canSubmit={canSubmit} canSubmit={canSubmit}

View File

@@ -1,7 +1,7 @@
import { useForm } from '@inertiajs/react'; import { useForm } from '@inertiajs/react';
import { route } from '@izzyjs/route/client'; import { route } from '@izzyjs/route/client';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MantineFormLink from '~/components/form/form_link'; import { FormLink } from '~/components/form/form_link';
import { LinkWithCollection } from '~/types/app'; import { LinkWithCollection } from '~/types/app';
export default function DeleteLinkPage({ link }: { link: LinkWithCollection }) { export default function DeleteLinkPage({ link }: { link: LinkWithCollection }) {
@@ -22,7 +22,7 @@ export default function DeleteLinkPage({ link }: { link: LinkWithCollection }) {
}; };
return ( return (
<MantineFormLink <FormLink
title={t('link.delete')} title={t('link.delete')}
textSubmitButton={t('form.delete')} textSubmitButton={t('form.delete')}
canSubmit={!processing} canSubmit={!processing}

View File

@@ -2,7 +2,7 @@ import { useForm } from '@inertiajs/react';
import { route } from '@izzyjs/route/client'; import { route } from '@izzyjs/route/client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MantineFormLink from '~/components/form/form_link'; import { FormLink } from '~/components/form/form_link';
import { isValidHttpUrl } from '~/lib/navigation'; import { isValidHttpUrl } from '~/lib/navigation';
import { Collection, Link } from '~/types/app'; import { Collection, Link } from '~/types/app';
@@ -50,7 +50,7 @@ export default function EditLinkPage({
}; };
return ( return (
<MantineFormLink <FormLink
title={t('link.edit')} title={t('link.edit')}
textSubmitButton={t('form.update')} textSubmitButton={t('form.update')}
canSubmit={canSubmit} canSubmit={canSubmit}

View File

@@ -1,6 +1,6 @@
{ {
"name": "my-links", "name": "my-links",
"version": "3.2.0", "version": "3.3.1",
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
@@ -44,69 +44,69 @@
"@adonisjs/eslint-config": "2.1.2", "@adonisjs/eslint-config": "2.1.2",
"@adonisjs/prettier-config": "^1.4.5", "@adonisjs/prettier-config": "^1.4.5",
"@adonisjs/tsconfig": "^1.4.1", "@adonisjs/tsconfig": "^1.4.1",
"@faker-js/faker": "^9.9.0", "@faker-js/faker": "^10.1.0",
"@japa/assert": "^4.1.1", "@japa/assert": "^4.1.1",
"@japa/plugin-adonisjs": "^4.0.0", "@japa/plugin-adonisjs": "^4.0.0",
"@japa/runner": "^4.4.0", "@japa/runner": "^4.4.0",
"@swc/core": "^1.13.3", "@swc/core": "^1.15.1",
"@tuyau/utils": "^0.0.9", "@tuyau/utils": "^0.0.9",
"@types/luxon": "^3.7.1", "@types/luxon": "^3.7.1",
"@types/node": "^24.3.0", "@types/node": "^24.10.0",
"@types/react": "^19.1.10", "@types/react": "^19.2.2",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.2.2",
"@typescript-eslint/eslint-plugin": "^8.40.0", "@typescript-eslint/eslint-plugin": "^8.46.3",
"@vite-pwa/assets-generator": "^1.0.0", "@vite-pwa/assets-generator": "^1.0.2",
"eslint": "^9.33.0", "eslint": "^9.39.1",
"hot-hook": "^0.4.0", "hot-hook": "^0.4.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.1.5", "lint-staged": "^16.2.6",
"pino-pretty": "^13.1.1", "pino-pretty": "^13.1.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"release-it": "^19.0.4", "release-it": "^19.0.6",
"ts-node-maintained": "^10.9.6", "ts-node-maintained": "^10.9.6",
"typescript": "~5.9.2", "typescript": "~5.9.3",
"vite": "^7.1.3" "vite": "^7.2.2"
}, },
"dependencies": { "dependencies": {
"@adonisjs/ally": "^5.1.0", "@adonisjs/ally": "^5.1.1",
"@adonisjs/auth": "^9.4.2", "@adonisjs/auth": "^9.5.1",
"@adonisjs/core": "^6.19.0", "@adonisjs/core": "^6.19.1",
"@adonisjs/cors": "^2.2.1", "@adonisjs/cors": "^2.2.1",
"@adonisjs/inertia": "^3.1.1", "@adonisjs/inertia": "^3.1.1",
"@adonisjs/lucid": "^21.8.0", "@adonisjs/lucid": "^21.8.1",
"@adonisjs/session": "^7.5.1", "@adonisjs/session": "^7.5.1",
"@adonisjs/shield": "^8.2.0", "@adonisjs/shield": "^8.2.0",
"@adonisjs/static": "^1.1.1", "@adonisjs/static": "^1.1.1",
"@adonisjs/vite": "^4.0.0", "@adonisjs/vite": "^4.0.0",
"@inertiajs/react": "^2.1.2", "@inertiajs/react": "^2.2.15",
"@izzyjs/route": "^1.2.0", "@izzyjs/route": "^2.1.7",
"@mantine/core": "^8.2.5", "@mantine/core": "^8.3.6",
"@mantine/hooks": "^8.2.5", "@mantine/hooks": "^8.3.6",
"@mantine/modals": "^8.2.5", "@mantine/modals": "^8.3.6",
"@mantine/spotlight": "^8.2.5", "@mantine/spotlight": "^8.3.6",
"@tuyau/client": "^0.2.10", "@tuyau/client": "^0.2.10",
"@tuyau/core": "^0.4.2", "@tuyau/core": "^0.4.2",
"@tuyau/inertia": "^0.0.15", "@tuyau/inertia": "^0.0.15",
"@vinejs/vine": "^3.0.1", "@vinejs/vine": "^4.1.0",
"@vitejs/plugin-react-oxc": "^0.3.0", "@vitejs/plugin-react": "^5.1.0",
"bentocache": "^1.5.0", "bentocache": "^1.5.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.19",
"edge.js": "^6.3.0", "edge.js": "^6.3.0",
"i18next": "^25.4.0", "i18next": "^25.6.1",
"knex": "^3.1.0", "knex": "^3.1.0",
"luxon": "^3.7.1", "luxon": "^3.7.2",
"node-html-parser": "^7.0.1", "node-html-parser": "^7.0.1",
"pg": "^8.16.3", "pg": "^8.16.3",
"react": "^19.1.1", "react": "^19.2.0",
"react-dom": "^19.1.1", "react-dom": "^19.2.0",
"react-i18next": "^15.7.0", "react-i18next": "^16.2.4",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"vite-plugin-pwa": "^1.0.3", "vite-plugin-pwa": "^1.1.0",
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
"hotHook": { "hotHook": {
@@ -129,11 +129,6 @@
"*.js,*.ts,*.jsx,*.tsx": "eslint --cache --fix" "*.js,*.ts,*.jsx,*.tsx": "eslint --cache --fix"
}, },
"volta": { "volta": {
"node": "24.6.0" "node": "24.11.0"
},
"pnpm": {
"overrides": {
"vite": "npm:rolldown-vite@latest"
}
} }
} }

2203
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
import { assert } from '@japa/assert';
import app from '@adonisjs/core/services/app'; import app from '@adonisjs/core/services/app';
import type { Config } from '@japa/runner/types';
import { pluginAdonisJS } from '@japa/plugin-adonisjs';
import testUtils from '@adonisjs/core/services/test_utils'; import testUtils from '@adonisjs/core/services/test_utils';
import { izzyRoutePlugin } from '@izzyjs/route/plugins/japa'; import { izzyRoutePlugin } from '@izzyjs/route/plugins/japa';
import { assert } from '@japa/assert';
import { pluginAdonisJS } from '@japa/plugin-adonisjs';
import type { Config } from '@japa/runner/types';
/** /**
* This file is imported by the "bin/test.ts" entrypoint file * This file is imported by the "bin/test.ts" entrypoint file
@@ -16,7 +16,7 @@ import { izzyRoutePlugin } from '@izzyjs/route/plugins/japa';
export const plugins: Config['plugins'] = [ export const plugins: Config['plugins'] = [
assert(), assert(),
pluginAdonisJS(app), pluginAdonisJS(app),
izzyRoutePlugin(), izzyRoutePlugin(app),
]; ];
/** /**

View File

@@ -6,7 +6,7 @@ import {
import { getDirname } from '@adonisjs/core/helpers'; import { getDirname } from '@adonisjs/core/helpers';
import inertia from '@adonisjs/inertia/client'; import inertia from '@adonisjs/inertia/client';
import adonisjs from '@adonisjs/vite/client'; import adonisjs from '@adonisjs/vite/client';
import react from '@vitejs/plugin-react-oxc'; import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';