chore: init adonis
@@ -1,13 +0,0 @@
|
||||
node_modules
|
||||
.env
|
||||
.next
|
||||
.vscode
|
||||
example.env
|
||||
|
||||
# Additional good to have ignores for dockerignore
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
*.md
|
||||
.git
|
||||
.gitignore
|
||||
@@ -1,10 +1,22 @@
|
||||
root = true
|
||||
# http://editorconfig.org
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
tab_width = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.json]
|
||||
insert_final_newline = unset
|
||||
|
||||
[**.min.js]
|
||||
indent_style = unset
|
||||
insert_final_newline = unset
|
||||
|
||||
[MakeFile]
|
||||
indent_style = space
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
||||
TZ=UTC
|
||||
PORT=3333
|
||||
HOST=localhost
|
||||
LOG_LEVEL=info
|
||||
APP_KEY=sLoJth45JD1vcS8n92Y2JUd8w3OL4HQb
|
||||
NODE_ENV=development
|
||||
SESSION_DRIVER=cookie
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "next",
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off"
|
||||
}
|
||||
}
|
||||
2
.gitattributes
vendored
@@ -1,2 +0,0 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
53
.github/workflows/CD.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: CI/CD prod
|
||||
|
||||
on:
|
||||
release:
|
||||
types: published
|
||||
|
||||
jobs:
|
||||
push_image_to_docker_hub:
|
||||
name: Build and push image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5.5.1
|
||||
with:
|
||||
images: sonny93/my-links
|
||||
flavor: latest=true
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5.3.0
|
||||
with:
|
||||
file: ./Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
|
||||
execute_commands_via_ssh:
|
||||
name: Pull latest docker image and start up the application with Docker Compose
|
||||
runs-on: ubuntu-latest
|
||||
needs: push_image_to_docker_hub
|
||||
steps:
|
||||
- name: Executing remote ssh commands
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
port: ${{ secrets.SSH_PORT }}
|
||||
username: ${{ secrets.SSH_USERNAME }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
script: |
|
||||
cd /infra/my-links
|
||||
sh startup.sh
|
||||
56
.gitignore
vendored
@@ -1,45 +1,25 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# Dependencies and AdonisJS build
|
||||
node_modules
|
||||
build
|
||||
tmp
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
# Secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.production.local
|
||||
.env.development.local
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
# Frontend assets compiled code
|
||||
public/assets
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
# Build tools specific
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
# Editors specific
|
||||
.fleet
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# sitemap files (they are generated by postbuild script)
|
||||
public/sitemap*
|
||||
public/robots.txt
|
||||
|
||||
# pwa static files
|
||||
/public/sw*
|
||||
/public/workbox-*
|
||||
# Platform specific
|
||||
.DS_Store
|
||||
|
||||
1
.husky/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
63
.idea/codeStyles/Project.xml
generated
@@ -1,63 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
<option name="HTML_QUOTE_STYLE" value="Single" />
|
||||
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
5
.idea/misc.xml
generated
@@ -1,5 +0,0 @@
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/my-links.iml" filepath="$PROJECT_DIR$/.idea/my-links.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/my-links.iml
generated
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
7
.idea/prettier.xml
generated
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||
<option name="myRunOnSave" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
21
.prettierrc
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"experimentalTernaries": false,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"singleAttributePerLine": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"proseWrap": "preserve",
|
||||
"insertPragma": false,
|
||||
"printWidth": 80,
|
||||
"requirePragma": false,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"before:init": [
|
||||
"npm run lint"
|
||||
]
|
||||
},
|
||||
"git": {
|
||||
"commitMessage": "chore: release v${version}"
|
||||
},
|
||||
"github": {
|
||||
"release": true
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
}
|
||||
}
|
||||
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"discord.enabled": false
|
||||
}
|
||||
54
Dockerfile
@@ -1,54 +0,0 @@
|
||||
# Source : https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
|
||||
|
||||
FROM node:18-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm pkg delete scripts.prepare
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
RUN npx prisma generate
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
# set hostname to localhost
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
CMD npx prisma migrate deploy && node server.js
|
||||
14
Makefile
@@ -1,14 +0,0 @@
|
||||
db:
|
||||
docker compose -f dev.docker-compose.yml up -d --wait
|
||||
|
||||
dev: db
|
||||
npx prisma db push
|
||||
npx prisma generate
|
||||
npm run dev
|
||||
|
||||
release:
|
||||
npm run release
|
||||
|
||||
prod:
|
||||
-docker network create mylinks_app
|
||||
docker compose up -d --build --wait
|
||||
66
README.md
@@ -1,66 +0,0 @@
|
||||
# MyLinks
|
||||
|
||||
MyLinks is a tool that lets you manage your favorite links in an intuitive interface.
|
||||
Free and open source software, focused on privacy and self-hosting.
|
||||
|
||||
# Setup
|
||||
|
||||
Copy `example.env` file as `.env` and edit the properties.
|
||||
|
||||
```
|
||||
cp example.env .env
|
||||
```
|
||||
|
||||
## Dev
|
||||
|
||||
Leave the `DATABASE_URL` property filled
|
||||
|
||||
```
|
||||
cd docker
|
||||
make start-dev
|
||||
cd ..
|
||||
npx prisma db push
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Prod
|
||||
|
||||
If you want to use your own database leave, the `DATABASE_URL` property filled in `docker/docker-compose.yml` with your database credentials, otherwise you'll have to delete it.
|
||||
|
||||
```shell
|
||||
cd docker
|
||||
make build
|
||||
make start-prod
|
||||
```
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
Env var to define :
|
||||
|
||||
```shell
|
||||
DOCKER_USERNAME="Your docker username"
|
||||
DOCKER_PASSWORD="Your docker password"
|
||||
SSH_HOST="Your SSH host"
|
||||
SSH_PORT="Your SSH port" # use port 22 if you are using the default value
|
||||
SSH_USERNAME="Your SSH username" # private key
|
||||
SSH_KEY="Your SSH key" # see below
|
||||
```
|
||||
|
||||
> As a good practice, SSH Key should be generated on local machine instead of target/server/remote machine
|
||||
|
||||
Generate :
|
||||
|
||||
```shell
|
||||
ssh-keygen -t rsa -b 4096
|
||||
# you can save the file in your current folder since you're not supposed to use it personnaly (its purpose is only to be used by CI/CD)
|
||||
```
|
||||
|
||||
Copy :
|
||||
|
||||
```shell
|
||||
cat ./id_rsa.pub | ssh b@B 'cat >> ~/.ssh/authorized_keys'
|
||||
# or
|
||||
ssh-copy-id -i ./id_rsa.pub user@host
|
||||
```
|
||||
|
||||
> Source: https://github.com/appleboy/ssh-action#setting-up-a-ssh-key
|
||||
28
ace.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JavaScript entrypoint for running ace commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| DO NOT MODIFY THIS FILE AS IT WILL BE OVERRIDDEN DURING THE BUILD
|
||||
| PROCESS.
|
||||
|
|
||||
| See docs.adonisjs.com/guides/typescript-build-process#creating-production-build
|
||||
|
|
||||
| Since, we cannot run TypeScript source code using "node" binary, we need
|
||||
| a JavaScript entrypoint to run ace commands.
|
||||
|
|
||||
| This file registers the "ts-node/esm" hook with the Node.js module system
|
||||
| and then imports the "bin/console.ts" file.
|
||||
|
|
||||
*/
|
||||
|
||||
/**
|
||||
* Register hook to process TypeScript files using ts-node
|
||||
*/
|
||||
import { register } from 'node:module';
|
||||
register('ts-node/esm', import.meta.url);
|
||||
|
||||
/**
|
||||
* Import ace console entrypoint
|
||||
*/
|
||||
await import('./bin/console.js');
|
||||
102
adonisrc.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { defineConfig } from '@adonisjs/core/app';
|
||||
|
||||
export default defineConfig({
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of ace commands to register from packages. The application commands
|
||||
| will be scanned automatically from the "./commands" directory.
|
||||
|
|
||||
*/
|
||||
commands: [() => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands')],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Service providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of service providers to import and register when booting the
|
||||
| application
|
||||
|
|
||||
*/
|
||||
providers: [
|
||||
() => import('@adonisjs/core/providers/app_provider'),
|
||||
() => import('@adonisjs/core/providers/hash_provider'),
|
||||
{
|
||||
file: () => import('@adonisjs/core/providers/repl_provider'),
|
||||
environment: ['repl', 'test'],
|
||||
},
|
||||
() => import('@adonisjs/core/providers/vinejs_provider'),
|
||||
() => import('@adonisjs/core/providers/edge_provider'),
|
||||
() => import('@adonisjs/session/session_provider'),
|
||||
() => import('@adonisjs/vite/vite_provider'),
|
||||
() => import('@adonisjs/shield/shield_provider'),
|
||||
() => import('@adonisjs/static/static_provider'),
|
||||
() => import('@adonisjs/cors/cors_provider'),
|
||||
() => import('@adonisjs/lucid/database_provider'),
|
||||
() => import('@adonisjs/auth/auth_provider'),
|
||||
() => import('@adonisjs/inertia/inertia_provider'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Preloads
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of modules to import before starting the application.
|
||||
|
|
||||
*/
|
||||
preloads: [() => import('#start/routes'), () => import('#start/kernel')],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Tests
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| List of test suites to organize tests by their type. Feel free to remove
|
||||
| and add additional suites.
|
||||
|
|
||||
*/
|
||||
tests: {
|
||||
suites: [
|
||||
{
|
||||
files: ['tests/unit/**/*.spec(.ts|.js)'],
|
||||
name: 'unit',
|
||||
timeout: 2000,
|
||||
},
|
||||
{
|
||||
files: ['tests/functional/**/*.spec(.ts|.js)'],
|
||||
name: 'functional',
|
||||
timeout: 30000,
|
||||
},
|
||||
],
|
||||
forceExit: false,
|
||||
},
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Metafiles
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| A collection of files you want to copy to the build folder when creating
|
||||
| the production build.
|
||||
|
|
||||
*/
|
||||
metaFiles: [
|
||||
{
|
||||
pattern: 'resources/views/**/*.edge',
|
||||
reloadServer: false,
|
||||
},
|
||||
{
|
||||
pattern: 'public/**',
|
||||
reloadServer: false,
|
||||
},
|
||||
],
|
||||
|
||||
assetsBundler: false,
|
||||
unstable_assembler: {
|
||||
onBuildStarting: [() => import('@adonisjs/vite/build_hook')],
|
||||
},
|
||||
});
|
||||
45
app/exceptions/handler.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import app from '@adonisjs/core/services/app';
|
||||
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http';
|
||||
import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http';
|
||||
|
||||
export default class HttpExceptionHandler extends ExceptionHandler {
|
||||
/**
|
||||
* In debug mode, the exception handler will display verbose errors
|
||||
* with pretty printed stack traces.
|
||||
*/
|
||||
protected debug = !app.inProduction;
|
||||
|
||||
/**
|
||||
* Status pages are used to display a custom HTML pages for certain error
|
||||
* codes. You might want to enable them in production only, but feel
|
||||
* free to enable them in development as well.
|
||||
*/
|
||||
protected renderStatusPages = app.inProduction;
|
||||
|
||||
/**
|
||||
* Status pages is a collection of error code range and a callback
|
||||
* to return the HTML contents to send as a response.
|
||||
*/
|
||||
protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
|
||||
'404': (error, { inertia }) => inertia.render('errors/not_found', { error }),
|
||||
'500..599': (error, { inertia }) => inertia.render('errors/server_error', { error }),
|
||||
};
|
||||
|
||||
/**
|
||||
* The method is used for handling errors and returning
|
||||
* response to the client
|
||||
*/
|
||||
async handle(error: unknown, ctx: HttpContext) {
|
||||
return super.handle(error, ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* The method is used to report error to the logging service or
|
||||
* the a third party error monitoring service.
|
||||
*
|
||||
* @note You should not attempt to send a response from this method.
|
||||
*/
|
||||
async report(error: unknown, ctx: HttpContext) {
|
||||
return super.report(error, ctx);
|
||||
}
|
||||
}
|
||||
25
app/middleware/auth_middleware.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import type { NextFn } from '@adonisjs/core/types/http';
|
||||
import type { Authenticators } from '@adonisjs/auth/types';
|
||||
|
||||
/**
|
||||
* Auth middleware is used authenticate HTTP requests and deny
|
||||
* access to unauthenticated users.
|
||||
*/
|
||||
export default class AuthMiddleware {
|
||||
/**
|
||||
* The URL to redirect to, when authentication fails
|
||||
*/
|
||||
redirectTo = '/login';
|
||||
|
||||
async handle(
|
||||
ctx: HttpContext,
|
||||
next: NextFn,
|
||||
options: {
|
||||
guards?: (keyof Authenticators)[];
|
||||
} = {}
|
||||
) {
|
||||
await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo });
|
||||
return next();
|
||||
}
|
||||
}
|
||||
19
app/middleware/container_bindings_middleware.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Logger } from '@adonisjs/core/logger';
|
||||
import { HttpContext } from '@adonisjs/core/http';
|
||||
import { NextFn } from '@adonisjs/core/types/http';
|
||||
|
||||
/**
|
||||
* The container bindings middleware binds classes to their request
|
||||
* specific value using the container resolver.
|
||||
*
|
||||
* - We bind "HttpContext" class to the "ctx" object
|
||||
* - And bind "Logger" class to the "ctx.logger" object
|
||||
*/
|
||||
export default class ContainerBindingsMiddleware {
|
||||
handle(ctx: HttpContext, next: NextFn) {
|
||||
ctx.containerResolver.bindValue(HttpContext, ctx);
|
||||
ctx.containerResolver.bindValue(Logger, ctx.logger);
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
31
app/middleware/guest_middleware.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import type { NextFn } from '@adonisjs/core/types/http';
|
||||
import type { Authenticators } from '@adonisjs/auth/types';
|
||||
|
||||
/**
|
||||
* Guest middleware is used to deny access to routes that should
|
||||
* be accessed by unauthenticated users.
|
||||
*
|
||||
* For example, the login page should not be accessible if the user
|
||||
* is already logged-in
|
||||
*/
|
||||
export default class GuestMiddleware {
|
||||
/**
|
||||
* The URL to redirect to when user is logged-in
|
||||
*/
|
||||
redirectTo = '/';
|
||||
|
||||
async handle(
|
||||
ctx: HttpContext,
|
||||
next: NextFn,
|
||||
options: { guards?: (keyof Authenticators)[] } = {}
|
||||
) {
|
||||
for (let guard of options.guards || [ctx.auth.defaultGuard]) {
|
||||
if (await ctx.auth.use(guard).check()) {
|
||||
return ctx.response.redirect(this.redirectTo, true);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
30
app/models/user.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import hash from '@adonisjs/core/services/hash';
|
||||
import { compose } from '@adonisjs/core/helpers';
|
||||
import { BaseModel, column } from '@adonisjs/lucid/orm';
|
||||
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid';
|
||||
|
||||
const AuthFinder = withAuthFinder(() => hash.use('scrypt'), {
|
||||
uids: ['email'],
|
||||
passwordColumnName: 'password',
|
||||
});
|
||||
|
||||
export default class User extends compose(BaseModel, AuthFinder) {
|
||||
@column({ isPrimary: true })
|
||||
declare id: number;
|
||||
|
||||
@column()
|
||||
declare fullName: string | null;
|
||||
|
||||
@column()
|
||||
declare email: string;
|
||||
|
||||
@column()
|
||||
declare password: string;
|
||||
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime;
|
||||
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime | null;
|
||||
}
|
||||
47
bin/console.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Ace entry point
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The "console.ts" file is the entrypoint for booting the AdonisJS
|
||||
| command-line framework and executing commands.
|
||||
|
|
||||
| Commands do not boot the application, unless the currently running command
|
||||
| has "options.startApp" flag set to true.
|
||||
|
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { Ignitor, prettyPrintError } from '@adonisjs/core';
|
||||
|
||||
/**
|
||||
* URL to the application root. AdonisJS need it to resolve
|
||||
* paths to file and directories for scaffolding commands
|
||||
*/
|
||||
const APP_ROOT = new URL('../', import.meta.url);
|
||||
|
||||
/**
|
||||
* The importer is used to import files in context of the
|
||||
* application.
|
||||
*/
|
||||
const IMPORTER = (filePath: string) => {
|
||||
if (filePath.startsWith('./') || filePath.startsWith('../')) {
|
||||
return import(new URL(filePath, APP_ROOT).href);
|
||||
}
|
||||
return import(filePath);
|
||||
};
|
||||
|
||||
new Ignitor(APP_ROOT, { importer: IMPORTER })
|
||||
.tap((app) => {
|
||||
app.booting(async () => {
|
||||
await import('#start/env');
|
||||
});
|
||||
app.listen('SIGTERM', () => app.terminate());
|
||||
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate());
|
||||
})
|
||||
.ace()
|
||||
.handle(process.argv.splice(2))
|
||||
.catch((error) => {
|
||||
process.exitCode = 1;
|
||||
prettyPrintError(error);
|
||||
});
|
||||
45
bin/server.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP server entrypoint
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The "server.ts" file is the entrypoint for starting the AdonisJS HTTP
|
||||
| server. Either you can run this file directly or use the "serve"
|
||||
| command to run this file and monitor file changes
|
||||
|
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { Ignitor, prettyPrintError } from '@adonisjs/core';
|
||||
|
||||
/**
|
||||
* URL to the application root. AdonisJS need it to resolve
|
||||
* paths to file and directories for scaffolding commands
|
||||
*/
|
||||
const APP_ROOT = new URL('../', import.meta.url);
|
||||
|
||||
/**
|
||||
* The importer is used to import files in context of the
|
||||
* application.
|
||||
*/
|
||||
const IMPORTER = (filePath: string) => {
|
||||
if (filePath.startsWith('./') || filePath.startsWith('../')) {
|
||||
return import(new URL(filePath, APP_ROOT).href);
|
||||
}
|
||||
return import(filePath);
|
||||
};
|
||||
|
||||
new Ignitor(APP_ROOT, { importer: IMPORTER })
|
||||
.tap((app) => {
|
||||
app.booting(async () => {
|
||||
await import('#start/env');
|
||||
});
|
||||
app.listen('SIGTERM', () => app.terminate());
|
||||
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate());
|
||||
})
|
||||
.httpServer()
|
||||
.start()
|
||||
.catch((error) => {
|
||||
process.exitCode = 1;
|
||||
prettyPrintError(error);
|
||||
});
|
||||
62
bin/test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test runner entrypoint
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The "test.ts" file is the entrypoint for running tests using Japa.
|
||||
|
|
||||
| Either you can run this file directly or use the "test"
|
||||
| command to run this file and monitor file changes.
|
||||
|
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { Ignitor, prettyPrintError } from '@adonisjs/core';
|
||||
import { configure, processCLIArgs, run } from '@japa/runner';
|
||||
|
||||
/**
|
||||
* URL to the application root. AdonisJS need it to resolve
|
||||
* paths to file and directories for scaffolding commands
|
||||
*/
|
||||
const APP_ROOT = new URL('../', import.meta.url);
|
||||
|
||||
/**
|
||||
* The importer is used to import files in context of the
|
||||
* application.
|
||||
*/
|
||||
const IMPORTER = (filePath: string) => {
|
||||
if (filePath.startsWith('./') || filePath.startsWith('../')) {
|
||||
return import(new URL(filePath, APP_ROOT).href);
|
||||
}
|
||||
return import(filePath);
|
||||
};
|
||||
|
||||
new Ignitor(APP_ROOT, { importer: IMPORTER })
|
||||
.tap((app) => {
|
||||
app.booting(async () => {
|
||||
await import('#start/env');
|
||||
});
|
||||
app.listen('SIGTERM', () => app.terminate());
|
||||
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate());
|
||||
})
|
||||
.testRunner()
|
||||
.configure(async (app) => {
|
||||
const { runnerHooks, ...config } = await import('../tests/bootstrap.js');
|
||||
|
||||
processCLIArgs(process.argv.splice(2));
|
||||
configure({
|
||||
...app.rcFile.tests,
|
||||
...config,
|
||||
...{
|
||||
setup: runnerHooks.setup,
|
||||
teardown: runnerHooks.teardown.concat([() => app.terminate()]),
|
||||
},
|
||||
});
|
||||
})
|
||||
.run(() => run())
|
||||
.catch((error) => {
|
||||
process.exitCode = 1;
|
||||
prettyPrintError(error);
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
const config = {
|
||||
url: process.env.NEXT_PUBLIC_SITE_URL,
|
||||
name: 'MyLinks',
|
||||
description:
|
||||
'MyLinks is a free and open source software, that lets you manage your bookmarks in an intuitive interface',
|
||||
};
|
||||
|
||||
export default config;
|
||||
40
config/app.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import env from '#start/env';
|
||||
import app from '@adonisjs/core/services/app';
|
||||
import { Secret } from '@adonisjs/core/helpers';
|
||||
import { defineConfig } from '@adonisjs/core/http';
|
||||
|
||||
/**
|
||||
* The app key is used for encrypting cookies, generating signed URLs,
|
||||
* and by the "encryption" module.
|
||||
*
|
||||
* The encryption module will fail to decrypt data if the key is lost or
|
||||
* changed. Therefore it is recommended to keep the app key secure.
|
||||
*/
|
||||
export const appKey = new Secret(env.get('APP_KEY'));
|
||||
|
||||
/**
|
||||
* The configuration settings used by the HTTP server
|
||||
*/
|
||||
export const http = defineConfig({
|
||||
generateRequestId: true,
|
||||
allowMethodSpoofing: false,
|
||||
|
||||
/**
|
||||
* Enabling async local storage will let you access HTTP context
|
||||
* from anywhere inside your application.
|
||||
*/
|
||||
useAsyncLocalStorage: false,
|
||||
|
||||
/**
|
||||
* Manage cookies configuration. The settings for the session id cookie are
|
||||
* defined inside the "config/session.ts" file.
|
||||
*/
|
||||
cookie: {
|
||||
domain: '',
|
||||
path: '/',
|
||||
maxAge: '2h',
|
||||
httpOnly: true,
|
||||
secure: app.inProduction,
|
||||
sameSite: 'lax',
|
||||
},
|
||||
});
|
||||
28
config/auth.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from '@adonisjs/auth';
|
||||
import { InferAuthEvents, Authenticators } from '@adonisjs/auth/types';
|
||||
import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session';
|
||||
|
||||
const authConfig = defineConfig({
|
||||
default: 'web',
|
||||
guards: {
|
||||
web: sessionGuard({
|
||||
useRememberMeTokens: false,
|
||||
provider: sessionUserProvider({
|
||||
model: () => import('#models/user'),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export default authConfig;
|
||||
|
||||
/**
|
||||
* Inferring types from the configured auth
|
||||
* guards.
|
||||
*/
|
||||
declare module '@adonisjs/auth/types' {
|
||||
interface Authenticators extends InferAuthenticators<typeof authConfig> {}
|
||||
}
|
||||
declare module '@adonisjs/core/types' {
|
||||
interface EventsList extends InferAuthEvents<Authenticators> {}
|
||||
}
|
||||
55
config/bodyparser.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineConfig } from '@adonisjs/core/bodyparser';
|
||||
|
||||
const bodyParserConfig = defineConfig({
|
||||
/**
|
||||
* The bodyparser middleware will parse the request body
|
||||
* for the following HTTP methods.
|
||||
*/
|
||||
allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
|
||||
/**
|
||||
* Config for the "application/x-www-form-urlencoded"
|
||||
* content-type parser
|
||||
*/
|
||||
form: {
|
||||
convertEmptyStringsToNull: true,
|
||||
types: ['application/x-www-form-urlencoded'],
|
||||
},
|
||||
|
||||
/**
|
||||
* Config for the JSON parser
|
||||
*/
|
||||
json: {
|
||||
convertEmptyStringsToNull: true,
|
||||
types: [
|
||||
'application/json',
|
||||
'application/json-patch+json',
|
||||
'application/vnd.api+json',
|
||||
'application/csp-report',
|
||||
],
|
||||
},
|
||||
|
||||
/**
|
||||
* Config for the "multipart/form-data" content-type parser.
|
||||
* File uploads are handled by the multipart parser.
|
||||
*/
|
||||
multipart: {
|
||||
/**
|
||||
* Enabling auto process allows bodyparser middleware to
|
||||
* move all uploaded files inside the tmp folder of your
|
||||
* operating system
|
||||
*/
|
||||
autoProcess: true,
|
||||
convertEmptyStringsToNull: true,
|
||||
processManually: [],
|
||||
|
||||
/**
|
||||
* Maximum limit of data to parse including all files
|
||||
* and fields
|
||||
*/
|
||||
limit: '20mb',
|
||||
types: ['multipart/form-data'],
|
||||
},
|
||||
});
|
||||
|
||||
export default bodyParserConfig;
|
||||
19
config/cors.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from '@adonisjs/cors';
|
||||
|
||||
/**
|
||||
* Configuration options to tweak the CORS policy. The following
|
||||
* options are documented on the official documentation website.
|
||||
*
|
||||
* https://docs.adonisjs.com/guides/security/cors
|
||||
*/
|
||||
const corsConfig = defineConfig({
|
||||
enabled: true,
|
||||
origin: [],
|
||||
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
|
||||
headers: true,
|
||||
exposeHeaders: [],
|
||||
credentials: true,
|
||||
maxAge: 90,
|
||||
});
|
||||
|
||||
export default corsConfig;
|
||||
24
config/database.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import env from '#start/env';
|
||||
import { defineConfig } from '@adonisjs/lucid';
|
||||
|
||||
const dbConfig = defineConfig({
|
||||
connection: 'postgres',
|
||||
connections: {
|
||||
postgres: {
|
||||
client: 'pg',
|
||||
connection: {
|
||||
host: env.get('DB_HOST'),
|
||||
port: env.get('DB_PORT'),
|
||||
user: env.get('DB_USER'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
},
|
||||
migrations: {
|
||||
naturalSort: true,
|
||||
paths: ['database/migrations'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default dbConfig;
|
||||
24
config/hash.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineConfig, drivers } from '@adonisjs/core/hash';
|
||||
|
||||
const hashConfig = defineConfig({
|
||||
default: 'scrypt',
|
||||
|
||||
list: {
|
||||
scrypt: drivers.scrypt({
|
||||
cost: 16384,
|
||||
blockSize: 8,
|
||||
parallelization: 1,
|
||||
maxMemory: 33554432,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export default hashConfig;
|
||||
|
||||
/**
|
||||
* Inferring types for the list of hashers you have configured
|
||||
* in your application.
|
||||
*/
|
||||
declare module '@adonisjs/core/types' {
|
||||
export interface HashersList extends InferHashers<typeof hashConfig> {}
|
||||
}
|
||||
23
config/inertia.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from '@adonisjs/inertia';
|
||||
|
||||
export default defineConfig({
|
||||
/**
|
||||
* Path to the Edge view that will be used as the root view for Inertia responses
|
||||
*/
|
||||
rootView: 'inertia_layout',
|
||||
|
||||
/**
|
||||
* Data that should be shared with all rendered pages
|
||||
*/
|
||||
sharedData: {
|
||||
errors: (ctx) => ctx.session?.flashMessages.get('errors'),
|
||||
},
|
||||
|
||||
/**
|
||||
* Options for the server-side rendering
|
||||
*/
|
||||
ssr: {
|
||||
enabled: true,
|
||||
entrypoint: 'inertia/app/ssr.tsx',
|
||||
},
|
||||
});
|
||||
35
config/logger.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import env from '#start/env';
|
||||
import app from '@adonisjs/core/services/app';
|
||||
import { defineConfig, targets } from '@adonisjs/core/logger';
|
||||
|
||||
const loggerConfig = defineConfig({
|
||||
default: 'app',
|
||||
|
||||
/**
|
||||
* The loggers object can be used to define multiple loggers.
|
||||
* By default, we configure only one logger (named "app").
|
||||
*/
|
||||
loggers: {
|
||||
app: {
|
||||
enabled: true,
|
||||
name: env.get('APP_NAME'),
|
||||
level: env.get('LOG_LEVEL'),
|
||||
transport: {
|
||||
targets: targets()
|
||||
.pushIf(!app.inProduction, targets.pretty())
|
||||
.pushIf(app.inProduction, targets.file({ destination: 1 }))
|
||||
.toArray(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default loggerConfig;
|
||||
|
||||
/**
|
||||
* Inferring types for the list of loggers you have configured
|
||||
* in your application.
|
||||
*/
|
||||
declare module '@adonisjs/core/types' {
|
||||
export interface LoggersList extends InferLoggers<typeof loggerConfig> {}
|
||||
}
|
||||
48
config/session.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import env from '#start/env';
|
||||
import app from '@adonisjs/core/services/app';
|
||||
import { defineConfig, stores } from '@adonisjs/session';
|
||||
|
||||
const sessionConfig = defineConfig({
|
||||
enabled: true,
|
||||
cookieName: 'adonis-session',
|
||||
|
||||
/**
|
||||
* When set to true, the session id cookie will be deleted
|
||||
* once the user closes the browser.
|
||||
*/
|
||||
clearWithBrowser: false,
|
||||
|
||||
/**
|
||||
* Define how long to keep the session data alive without
|
||||
* any activity.
|
||||
*/
|
||||
age: '2h',
|
||||
|
||||
/**
|
||||
* Configuration for session cookie and the
|
||||
* cookie store
|
||||
*/
|
||||
cookie: {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: app.inProduction,
|
||||
sameSite: 'lax',
|
||||
},
|
||||
|
||||
/**
|
||||
* The store to use. Make sure to validate the environment
|
||||
* variable in order to infer the store name without any
|
||||
* errors.
|
||||
*/
|
||||
store: env.get('SESSION_DRIVER'),
|
||||
|
||||
/**
|
||||
* List of configured stores. Refer documentation to see
|
||||
* list of available stores and their config.
|
||||
*/
|
||||
stores: {
|
||||
cookie: stores.cookie(),
|
||||
},
|
||||
});
|
||||
|
||||
export default sessionConfig;
|
||||
51
config/shield.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { defineConfig } from '@adonisjs/shield';
|
||||
|
||||
const shieldConfig = defineConfig({
|
||||
/**
|
||||
* Configure CSP policies for your app. Refer documentation
|
||||
* to learn more
|
||||
*/
|
||||
csp: {
|
||||
enabled: false,
|
||||
directives: {},
|
||||
reportOnly: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* Configure CSRF protection options. Refer documentation
|
||||
* to learn more
|
||||
*/
|
||||
csrf: {
|
||||
enabled: true,
|
||||
exceptRoutes: [],
|
||||
enableXsrfCookie: true,
|
||||
methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
},
|
||||
|
||||
/**
|
||||
* Control how your website should be embedded inside
|
||||
* iFrames
|
||||
*/
|
||||
xFrame: {
|
||||
enabled: true,
|
||||
action: 'DENY',
|
||||
},
|
||||
|
||||
/**
|
||||
* Force browser to always use HTTPS
|
||||
*/
|
||||
hsts: {
|
||||
enabled: true,
|
||||
maxAge: '180 days',
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable browsers from sniffing the content type of a
|
||||
* response and always rely on the "content-type" header.
|
||||
*/
|
||||
contentTypeSniffing: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default shieldConfig;
|
||||
17
config/static.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from '@adonisjs/static';
|
||||
|
||||
/**
|
||||
* Configuration options to tweak the static files middleware.
|
||||
* The complete set of options are documented on the
|
||||
* official documentation website.
|
||||
*
|
||||
* https://docs.adonisjs.com/guides/static-assets
|
||||
*/
|
||||
const staticServerConfig = defineConfig({
|
||||
enabled: true,
|
||||
etag: true,
|
||||
lastModified: true,
|
||||
dotFiles: 'ignore',
|
||||
});
|
||||
|
||||
export default staticServerConfig;
|
||||
28
config/vite.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from '@adonisjs/vite';
|
||||
|
||||
const viteBackendConfig = defineConfig({
|
||||
/**
|
||||
* The output of vite will be written inside this
|
||||
* directory. The path should be relative from
|
||||
* the application root.
|
||||
*/
|
||||
buildDirectory: 'public/assets',
|
||||
|
||||
/**
|
||||
* The path to the manifest file generated by the
|
||||
* "vite build" command.
|
||||
*/
|
||||
manifestFile: 'public/assets/.vite/manifest.json',
|
||||
|
||||
/**
|
||||
* Feel free to change the value of the "assetsUrl" to
|
||||
* point to a CDN in production.
|
||||
*/
|
||||
assetsUrl: '/assets',
|
||||
|
||||
scriptAttributes: {
|
||||
defer: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default viteBackendConfig;
|
||||
21
database/migrations/1714218548323_create_users_table.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { BaseSchema } from '@adonisjs/lucid/schema';
|
||||
|
||||
export default class extends BaseSchema {
|
||||
protected tableName = 'users';
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id').notNullable();
|
||||
table.string('full_name').nullable();
|
||||
table.string('email', 254).notNullable().unique();
|
||||
table.string('password').notNullable();
|
||||
|
||||
table.timestamp('created_at').notNullable();
|
||||
table.timestamp('updated_at').nullable();
|
||||
});
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.dropTable(this.tableName);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
services:
|
||||
mysqldb:
|
||||
image: mysql:latest
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- '3306:3306'
|
||||
healthcheck:
|
||||
test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD
|
||||
start_period: 5s
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 55
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin:5
|
||||
restart: always
|
||||
environment:
|
||||
- PMA_HOST=mysqldb
|
||||
- PMA_PORT=3306
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- '8080:80'
|
||||
depends_on:
|
||||
- mysqldb
|
||||
@@ -1,42 +0,0 @@
|
||||
networks:
|
||||
mylinks_app:
|
||||
external: true
|
||||
|
||||
services:
|
||||
mylinks:
|
||||
restart: always
|
||||
container_name: MyLinks
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mylinks_db:3306/${MYSQL_DATABASE}
|
||||
networks:
|
||||
- mylinks_app
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
'CMD-SHELL',
|
||||
'wget --spider --tries=1 --no-verbose http://0.0.0.0:3000',
|
||||
]
|
||||
depends_on:
|
||||
mylinks_db:
|
||||
condition: service_started
|
||||
|
||||
mylinks_db:
|
||||
container_name: MyLinksDB
|
||||
image: mysql:latest
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
healthcheck:
|
||||
test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD
|
||||
start_period: 5s
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 55
|
||||
networks:
|
||||
- mylinks_app
|
||||
17
example.env
@@ -1,17 +0,0 @@
|
||||
MYSQL_USER="mluser"
|
||||
MYSQL_PASSWORD="root"
|
||||
MYSQL_ROOT_PASSWORD="root"
|
||||
MYSQL_DATABASE="mylinks"
|
||||
|
||||
DATABASE_URL="mysql://root:${MYSQL_ROOT_PASSWORD}@localhost:3306/${MYSQL_DATABASE}"
|
||||
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
|
||||
|
||||
NEXTAUTH_SECRET=""
|
||||
|
||||
GOOGLE_CLIENT_ID=""
|
||||
GOOGLE_CLIENT_SECRET=""
|
||||
|
||||
NEXT_PUBLIC_UMAMI_SCRIPT_ORIGIN="http://localhost:4000"
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=""
|
||||
20
inertia/app/app.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import '../css/app.css';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
import { createInertiaApp } from '@inertiajs/react';
|
||||
import { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'AdonisJS';
|
||||
|
||||
createInertiaApp({
|
||||
progress: { color: '#5468FF' },
|
||||
|
||||
title: (title) => `${title} - ${appName}`,
|
||||
|
||||
resolve: (name) => {
|
||||
return resolvePageComponent(`../pages/${name}.tsx`, import.meta.glob('../pages/**/*.tsx'));
|
||||
},
|
||||
|
||||
setup({ el, App, props }) {
|
||||
hydrateRoot(el, <App {...props} />);
|
||||
},
|
||||
});
|
||||
14
inertia/app/ssr.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import { createInertiaApp } from '@inertiajs/react';
|
||||
|
||||
export default function render(page: any) {
|
||||
return createInertiaApp({
|
||||
page,
|
||||
render: ReactDOMServer.renderToString,
|
||||
resolve: (name) => {
|
||||
const pages = import.meta.glob('../pages/**/*.tsx', { eager: true });
|
||||
return pages[`../pages/${name}.tsx`];
|
||||
},
|
||||
setup: ({ App, props }) => <App {...props} />,
|
||||
});
|
||||
}
|
||||
36
inertia/css/app.css
Normal file
@@ -0,0 +1,36 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
background-color: #f7f8fa;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
color: #46444c;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 42px;
|
||||
font-weight: 500;
|
||||
color: #5a45ff;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: #5a45ff;
|
||||
}
|
||||
11
inertia/pages/errors/not_found.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="title">Page not found</div>
|
||||
|
||||
<span>This page does not exist.</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
11
inertia/pages/errors/server_error.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function ServerError(props: { error: any }) {
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
<div className="title">Server Error</div>
|
||||
|
||||
<span>{props.error.message}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
18
inertia/pages/home.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Head } from '@inertiajs/react';
|
||||
|
||||
export default function Home(props: { version: number }) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Homepage" />
|
||||
|
||||
<div className="container">
|
||||
<div className="title">AdonisJS {props.version} x Inertia x React</div>
|
||||
|
||||
<span>
|
||||
Learn more about AdonisJS and Inertia.js by visiting the{' '}
|
||||
<a href="https://docs.adonisjs.com/guides/inertia">AdonisJS documentation</a>.
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
12
inertia/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@adonisjs/tsconfig/tsconfig.client.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.tsx"]
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import acceptLanguage from 'accept-language';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { i18n } from './next-i18next.config';
|
||||
|
||||
acceptLanguage.languages(i18n.locales);
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)'],
|
||||
};
|
||||
|
||||
const cookieName = 'i18next';
|
||||
|
||||
// Source : https://github.com/i18next/next-app-dir-i18next-example/blob/3d653a46ae33f46abc011b6186f7a4595b84129f/middleware.js
|
||||
export function middleware(req) {
|
||||
if (
|
||||
req.nextUrl.pathname.indexOf('icon') > -1 ||
|
||||
req.nextUrl.pathname.indexOf('chrome') > -1
|
||||
)
|
||||
return NextResponse.next();
|
||||
let lng;
|
||||
if (req.cookies.has(cookieName))
|
||||
lng = acceptLanguage.get(req.cookies.get(cookieName).value);
|
||||
if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'));
|
||||
if (!lng) lng = i18n.defaultLocale;
|
||||
|
||||
// Redirect if lng in path is not supported
|
||||
if (
|
||||
!i18n.locales.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
|
||||
!req.nextUrl.pathname.startsWith('/_next')
|
||||
) {
|
||||
return NextResponse.redirect(
|
||||
new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
|
||||
);
|
||||
}
|
||||
|
||||
if (req.headers.has('referer')) {
|
||||
const refererUrl = new URL(req.headers.get('referer'));
|
||||
const lngInReferer = i18n.locales.find((l) =>
|
||||
refererUrl.pathname.startsWith(`/${l}`),
|
||||
);
|
||||
const response = NextResponse.next();
|
||||
if (lngInReferer) response.cookies.set(cookieName, lngInReferer);
|
||||
return response;
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
5
next-env.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -1,10 +0,0 @@
|
||||
/** @type {import('next-i18next').UserConfig} */
|
||||
module.exports = {
|
||||
// debug: process.env.NODE_ENV === "development",
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
},
|
||||
reloadOnPrerender: process.env.NODE_ENV === 'development',
|
||||
returnNull: false,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
/** @type {import('next-sitemap').IConfig} */
|
||||
module.exports = {
|
||||
siteUrl: process.env.NEXTAUTH_URL || 'https://www.mylinks.app',
|
||||
generateRobotsTxt: true,
|
||||
output: 'standalone',
|
||||
exclude: ['/link/*', '/category/*'],
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
const { i18n } = require('./next-i18next.config');
|
||||
const {
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
PHASE_PRODUCTION_BUILD,
|
||||
} = require('next/constants');
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
i18n,
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ['@svgr/webpack'],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{ hostname: 'localhost' },
|
||||
{ hostname: 't3.gstatic.com' },
|
||||
{ hostname: 'lh3.googleusercontent.com' },
|
||||
{ hostname: 'www.mylinks.app' },
|
||||
],
|
||||
formats: ['image/webp'],
|
||||
},
|
||||
reactStrictMode: false,
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
module.exports = (phase) => {
|
||||
if (phase === PHASE_DEVELOPMENT_SERVER || phase === PHASE_PRODUCTION_BUILD) {
|
||||
const withPWA = require('@ducanh2912/next-pwa').default({
|
||||
dest: 'public',
|
||||
});
|
||||
return withPWA(nextConfig);
|
||||
}
|
||||
return nextConfig;
|
||||
};
|
||||
15422
package-lock.json
generated
137
package.json
@@ -1,64 +1,93 @@
|
||||
{
|
||||
"name": "my-links",
|
||||
"version": "1.3.0",
|
||||
"description": "MyLinks is a free and open source software, that lets you manage your bookmarks in an intuitive interface",
|
||||
"private": false,
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"postbuild": "next-sitemap",
|
||||
"prepare": "husky install",
|
||||
"release": "release-it"
|
||||
"start": "node bin/server.js",
|
||||
"build": "node ace build",
|
||||
"dev": "node ace serve --watch",
|
||||
"test": "node ace test",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ducanh2912/next-pwa": "^10.2.6",
|
||||
"@prisma/client": "^5.12.1",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/react-toggle": "^4.0.5",
|
||||
"accept-language": "^3.0.18",
|
||||
"clsx": "^2.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"framer-motion": "^11.0.28",
|
||||
"i18next": "^23.11.2",
|
||||
"next": "^14.2.1",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-i18next": "^15.2.0",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-tabs": "^6.0.2",
|
||||
"react-toggle": "^4.1.3",
|
||||
"sass": "^1.75.0",
|
||||
"sharp": "^0.33.3",
|
||||
"yup": "^1.4.0"
|
||||
"imports": {
|
||||
"#controllers/*": "./app/controllers/*.js",
|
||||
"#exceptions/*": "./app/exceptions/*.js",
|
||||
"#models/*": "./app/models/*.js",
|
||||
"#mails/*": "./app/mails/*.js",
|
||||
"#services/*": "./app/services/*.js",
|
||||
"#listeners/*": "./app/listeners/*.js",
|
||||
"#events/*": "./app/events/*.js",
|
||||
"#middleware/*": "./app/middleware/*.js",
|
||||
"#validators/*": "./app/validators/*.js",
|
||||
"#providers/*": "./providers/*.js",
|
||||
"#policies/*": "./app/policies/*.js",
|
||||
"#abilities/*": "./app/abilities/*.js",
|
||||
"#database/*": "./database/*.js",
|
||||
"#tests/*": "./tests/*.js",
|
||||
"#start/*": "./start/*.js",
|
||||
"#config/*": "./config/*.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adonisjs/assembler": "^7.5.1",
|
||||
"@adonisjs/eslint-config": "^1.3.0",
|
||||
"@adonisjs/tsconfig": "^1.3.0",
|
||||
"@japa/assert": "^3.0.0",
|
||||
"@japa/plugin-adonisjs": "^3.0.1",
|
||||
"@japa/runner": "^3.1.4",
|
||||
"@swc/core": "^1.4.16",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/react": "^18.2.78",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "14.2.1",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.2",
|
||||
"prisma": "^5.12.1",
|
||||
"release-it": "^17.2.0",
|
||||
"typescript": "5.4.5"
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"hot-hook": "^0.2.1",
|
||||
"pino-pretty": "^11.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.10"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": "eslint --cache --fix"
|
||||
"dependencies": {
|
||||
"@adonisjs/auth": "^9.2.0",
|
||||
"@adonisjs/core": "^6.8.0",
|
||||
"@adonisjs/cors": "^2.2.1",
|
||||
"@adonisjs/inertia": "1.0.0-25",
|
||||
"@adonisjs/lucid": "^20.5.1",
|
||||
"@adonisjs/session": "^7.4.0",
|
||||
"@adonisjs/shield": "^8.1.1",
|
||||
"@adonisjs/static": "^1.1.1",
|
||||
"@adonisjs/vite": "^3.0.0-11",
|
||||
"@inertiajs/react": "^1.0.16",
|
||||
"@vinejs/vine": "^2.0.0",
|
||||
"edge.js": "^6.0.2",
|
||||
"luxon": "^3.4.4",
|
||||
"pg": "^8.11.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"hotHook": {
|
||||
"boundaries": [
|
||||
"./app/controllers/**/*.ts",
|
||||
"./app/middleware/*.ts"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "@adonisjs/eslint-config/app"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"quoteProps": "consistent",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"printWidth": 100
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `user` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`google_id` VARCHAR(191) NOT NULL,
|
||||
`email` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `user_google_id_key`(`google_id`),
|
||||
UNIQUE INDEX `user_email_key`(`email`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `category` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`nextCategoryId` INTEGER NOT NULL DEFAULT 0,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `category_name_key`(`name`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `link` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`url` TEXT NOT NULL,
|
||||
`categoryId` INTEGER NOT NULL,
|
||||
`nextLinkId` INTEGER NOT NULL DEFAULT 0,
|
||||
`favorite` BOOLEAN NOT NULL DEFAULT false,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `link` ADD CONSTRAINT `link_categoryId_fkey` FOREIGN KEY (`categoryId`) REFERENCES `category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `nextCategoryId` on the `category` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `nextLinkId` on the `link` table. All the data in the column will be lost.
|
||||
- Added the required column `authorId` to the `category` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `category` DROP COLUMN `nextCategoryId`,
|
||||
ADD COLUMN `authorId` INTEGER NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `link` DROP COLUMN `nextLinkId`;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `category` ADD CONSTRAINT `category_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `authorId` to the `link` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `link` ADD COLUMN `authorId` INTEGER NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `link` ADD CONSTRAINT `link_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- DropIndex
|
||||
DROP INDEX `category_name_key` ON `category`;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `category` ADD COLUMN `nextId` INTEGER NULL;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `user` ADD COLUMN `name` VARCHAR(191) NULL AFTER `email`,
|
||||
ADD COLUMN `image` VARCHAR(191) NULL AFTER `name`;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `user` ADD COLUMN `is_admin` BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `link` ADD COLUMN `description` VARCHAR(255) NULL;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `category` ADD COLUMN `description` VARCHAR(255) NULL;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `category` ADD COLUMN `visibility` ENUM('private', 'public') NOT NULL DEFAULT 'private';
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "mysql"
|
||||
@@ -1,10 +0,0 @@
|
||||
-- set default next id for each category based on LEAD(id) (LEAD -> Next)
|
||||
|
||||
UPDATE category AS c
|
||||
JOIN (
|
||||
SELECT
|
||||
id,
|
||||
LEAD(id) OVER (PARTITION BY authorId ORDER BY id) AS nextCategoryId
|
||||
FROM category
|
||||
) AS n ON c.id = n.id
|
||||
SET c.nextId = n.nextCategoryId;
|
||||
@@ -1,71 +0,0 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
google_id String @unique
|
||||
email String @unique
|
||||
name String?
|
||||
image String?
|
||||
is_admin Boolean @default(false)
|
||||
|
||||
categories Category[]
|
||||
links Link[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
model Category {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(255)
|
||||
description String? @db.VarChar(255)
|
||||
links Link[]
|
||||
visibility Visibility @default(private)
|
||||
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
authorId Int
|
||||
|
||||
nextId Int?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("category")
|
||||
}
|
||||
|
||||
model Link {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(255)
|
||||
description String? @db.VarChar(255)
|
||||
url String @db.Text
|
||||
|
||||
category Category @relation(fields: [categoryId], references: [id])
|
||||
categoryId Int
|
||||
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
authorId Int
|
||||
|
||||
favorite Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("link")
|
||||
}
|
||||
|
||||
enum Visibility {
|
||||
private
|
||||
public
|
||||
}
|
||||
|
Before Width: | Height: | Size: 596 B |
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 106.813 73.354 L 86.501 73.355 C 86.501 84.421 95.594 93.398 106.813 93.398 C 124.952 93.398 139.996 108.476 139.996 126.656 L 140 126.656 C 140 144.835 124.956 159.913 106.816 159.913 L 0 159.913 L 62.001 200 L 62.001 189.977 C 62.001 184.441 66.476 179.956 72 179.956 L 106.816 179.956 L 106.816 179.962 C 136.14 179.962 159.999 156.048 159.999 126.656 C 159.999 97.266 136.14 73.354 106.813 73.354 Z" fill="#3f88c5" />
|
||||
<path d="M 93.184 126.646 L 113.498 126.645 C 113.498 115.579 104.402 106.603 93.184 106.603 C 75.042 106.603 60 91.524 60 73.344 C 60 55.165 75.044 40.09 93.184 40.09 L 200 40.09 L 137.999 0 L 137.999 10.023 C 137.999 15.559 133.524 20.044 128 20.044 L 93.184 20.044 L 93.184 20.039 C 63.86 20.039 40.001 43.951 40.001 73.344 L 39.997 73.344 C 39.997 102.735 63.855 126.646 93.184 126.646 Z" fill="#3f88c5" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 957 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30"><clipPath id="a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="b"><path d="M30 15h30v15zv15H0zH0V0zV0h30z"/></clipPath><g clip-path="url(#a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 527 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><path fill="#EC1920" d="M0 0h3v2H0z"/><path fill="#fff" d="M0 0h2v2H0z"/><path fill="#051440" d="M0 0h1v2H0z"/></svg>
|
||||
|
Before Width: | Height: | Size: 175 B |
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"hero": {
|
||||
"title": "Welcome to MyLinks",
|
||||
"cta": "Get started"
|
||||
},
|
||||
"category": {
|
||||
"title": "Create categories",
|
||||
"text": "Organize your bookmarks by categories to keep your links tidy and find them easily."
|
||||
},
|
||||
"link": {
|
||||
"title": "Manage Links",
|
||||
"text": "Add, edit, and manage your bookmarks with a simple and intuitive interface."
|
||||
},
|
||||
"search": {
|
||||
"title": "Search",
|
||||
"text": "Quickly find the bookmark you're looking for with the powerful search feature."
|
||||
},
|
||||
"extension": {
|
||||
"title": "Browser extension",
|
||||
"text": "Enhance your experience with the official MyLinks browser extension."
|
||||
},
|
||||
"contribute": {
|
||||
"title": "Contribute to MyLinks",
|
||||
"text": "Suggest improvements you would like to see on MyLinks."
|
||||
},
|
||||
"look-title": "Take a look",
|
||||
"website-screenshot-alt": "A screenshot of MyLinks"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"role": "Role",
|
||||
"created_at": "Created at",
|
||||
"updated_at": "Updated at",
|
||||
"admin": "Administrator",
|
||||
"user": "User",
|
||||
"users": "Users",
|
||||
"stats": "Statistics"
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"slogan": "Manage your links in the best possible way",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"back-home": "← Back to home page",
|
||||
"logout": "Logout",
|
||||
"login": "Login",
|
||||
"link": {
|
||||
"links": "Links",
|
||||
"link": "Link",
|
||||
"name": "Link name",
|
||||
"description": "Link description",
|
||||
"create": "Create a link",
|
||||
"edit": "Edit a link",
|
||||
"remove": "Delete a link",
|
||||
"remove-confirm": "Confirm deletion?"
|
||||
},
|
||||
"category": {
|
||||
"categories": "Categories",
|
||||
"category": "Category",
|
||||
"name": "Category name",
|
||||
"description": "Category description",
|
||||
"no-description": "No description",
|
||||
"visibility": "Public",
|
||||
"create": "Create a category",
|
||||
"edit": "Edit a category",
|
||||
"remove": "Delete a category",
|
||||
"remove-confirm": "Confirm deletion?",
|
||||
"remove-description": "You must delete all links in this category before you can delete this category."
|
||||
},
|
||||
"favorite": "Favorite",
|
||||
"no-item-found": "No item found",
|
||||
"search": "Search",
|
||||
"avatar": "{{name}}'s avatar",
|
||||
"generic-error": "Something went wrong",
|
||||
"generic-error-description": "An error has occurred, if this happens again please <a href=\"https://github.com/Sonny93/my-links\" target=\"_blank\">create an issue</a> with as much detail as possible.",
|
||||
"retry": "Retry",
|
||||
"privacy": "Privacy",
|
||||
"terms": "Terms of use",
|
||||
"language": {
|
||||
"fr": "Français",
|
||||
"en": "English"
|
||||
},
|
||||
"lang": "Language",
|
||||
"settings": "Settings",
|
||||
"profile": "Profile",
|
||||
"select-your-lang": "Change the language",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"footer": {
|
||||
"made_by": "Made with ❤\uFE0F by"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"select-category": "Please select a category",
|
||||
"or-create-one": "or create one",
|
||||
"no-link": "No link for <b>{{name}}</b>"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Authentication",
|
||||
"informative-text": "Authentication required to use MyLinks",
|
||||
"continue-with": "Continue with {{provider}}"
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"title": "Privacy Policy of MyLinks",
|
||||
"edited_at": "Last updated: {{date}}",
|
||||
"welcome": "Welcome to MyLinks, a free and open-source bookmark manager focused on privacy and self-hosting. This privacy policy aims to inform you about how we collect, use, and protect your data.",
|
||||
"collect": {
|
||||
"title": "1. Data Collection",
|
||||
"cookie": {
|
||||
"title": "1.1 Cookies",
|
||||
"description": "Cookies used on MyLinks are essential to ensure the proper functioning of the site. By continuing to use our service, you consent to the use of these cookies."
|
||||
},
|
||||
"user": {
|
||||
"title": "1.2 User Data",
|
||||
"description": "To create personalized categories and links and associate them with their author, we collect the following information:",
|
||||
"fields": ["Google ID", "Lastname", "Firstname", "Email", "Avatar"]
|
||||
}
|
||||
},
|
||||
"data_use": {
|
||||
"title": "2. Data Use",
|
||||
"description": "The collected data is neither resold nor used for purposes other than initially intended, namely the management of categories and links created by the user."
|
||||
},
|
||||
"data_storage": {
|
||||
"title": "3. Data Storage",
|
||||
"description": "Data is stored securely to protect your privacy.",
|
||||
"data_retention": {
|
||||
"title": "3.1 Data Retention Period",
|
||||
"description": "Functional data is retained until the user requests deletion. Once this request is made, the data will be permanently deleted."
|
||||
}
|
||||
},
|
||||
"user_rights": {
|
||||
"title": "4. User Rights",
|
||||
"description": "The user has the right to retrieve all their data at any time and/or request the complete deletion of their data."
|
||||
},
|
||||
"gdpr": {
|
||||
"title": "5. GDPR Compliance",
|
||||
"description": "MyLinks complies with the General Data Protection Regulation (GDPR) of the European Union."
|
||||
},
|
||||
"contact": {
|
||||
"title": "6. Contact",
|
||||
"description": "If you have any questions or concerns about our privacy policy, feel free to contact us at the following address:"
|
||||
},
|
||||
"footer": {
|
||||
"changes": "We reserve the right to update this privacy policy. We encourage you to regularly check this page to stay informed of any changes.",
|
||||
"thanks": "Thank you for using MyLinks!"
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"title": "Terms and Conditions of Use for MyLinks",
|
||||
"edited_at": "Last updated: {{date}}",
|
||||
"welcome": "Welcome to MyLinks, a free and open-source bookmark manager focused on privacy and self-hosting. By using this service, you agree to the terms and conditions of use outlined below. Please read them carefully.",
|
||||
"accept": {
|
||||
"title": "1. Acceptance of Terms",
|
||||
"description": "By accessing MyLinks and using our services, you agree to comply with these Terms and Conditions of Use."
|
||||
},
|
||||
"use": {
|
||||
"title": "2. Use of the Service",
|
||||
"account": {
|
||||
"title": "2.1 User Account",
|
||||
"description": "To access certain features of MyLinks, you will need to create a user account. You are responsible for the confidentiality of your account and credentials."
|
||||
},
|
||||
"allowed": {
|
||||
"title": "2.2 Authorized Use",
|
||||
"description": "You commit to using MyLinks in accordance with applicable laws and not violating the rights of third parties."
|
||||
},
|
||||
"user_content": {
|
||||
"title": "2.3 User Content",
|
||||
"description": "By posting content on MyLinks, you grant MyLinks a worldwide, non-exclusive, transferable, and free license to use, reproduce, distribute, and display this content."
|
||||
}
|
||||
},
|
||||
"personal_data": {
|
||||
"title": "3. Personal Data",
|
||||
"collect": {
|
||||
"title": "3.1 Collection and Use",
|
||||
"description": "The personal data collected is used in accordance with our <a>Privacy Policy</a>. By using MyLinks, you consent to this collection and use."
|
||||
},
|
||||
"suppress": {
|
||||
"title": "3.2 Account Deletion",
|
||||
"description": "You can request the deletion of your account at any time in accordance with our Privacy Policy."
|
||||
}
|
||||
},
|
||||
"responsibility_warranty": {
|
||||
"title": "4. Responsibilities and Warranties",
|
||||
"responsibility": {
|
||||
"title": "4.1 Responsibility",
|
||||
"description": "MyLinks cannot be held responsible for direct or indirect damages arising from the use of our services."
|
||||
},
|
||||
"warranty": {
|
||||
"title": "4.2 Warranties",
|
||||
"description": "MyLinks does not guarantee that the service will be free from errors or interruptions."
|
||||
}
|
||||
},
|
||||
"terms_changes": {
|
||||
"title": "5. Changes to the Terms",
|
||||
"description": "MyLinks reserves the right to modify these Terms and Conditions of Use at any time. Users will be notified of changes through a notification on the site."
|
||||
},
|
||||
"cancel": {
|
||||
"title": "6. Termination",
|
||||
"description": "MyLinks reserves the right to terminate or suspend your access to the service, with or without notice, in case of violation of these Terms and Conditions of Use."
|
||||
},
|
||||
"contact": {
|
||||
"title": "7. Contact",
|
||||
"description": "For any questions or concerns regarding these Terms and Conditions of Use, please contact us at the following address:"
|
||||
},
|
||||
"footer": {
|
||||
"changes": "We reserve the right to update these Terms and Conditions of Use. We encourage you to regularly check this page to stay informed of any changes.",
|
||||
"thanks": "Thank you for using MyLinks!"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"hero": {
|
||||
"title": "Bienvenue sur MyLinks",
|
||||
"cta": "Lancez-vous !"
|
||||
},
|
||||
"category": {
|
||||
"title": "Créer des catégories",
|
||||
"text": "Organisez vos favoris dans des catégories pour garder vos liens en ordre et les retrouver facilement."
|
||||
},
|
||||
"link": {
|
||||
"title": "Gérer les liens",
|
||||
"text": "Ajoutez, modifiez et gérez vos favoris à l'aide d'une interface simple et intuitive."
|
||||
},
|
||||
"search": {
|
||||
"title": "Rechercher",
|
||||
"text": "Trouvez rapidement vos liens favoris en utilisant la fonction de recherche."
|
||||
},
|
||||
"extension": {
|
||||
"title": "Extension de navigateur",
|
||||
"text": "Améliorez votre expérience avec l'extension de navigateur officielle MyLinks."
|
||||
},
|
||||
"contribute": {
|
||||
"title": "Contribuer à MyLinks",
|
||||
"text": "Proposez des améliorations que vous souhaiteriez voir sur MyLinks."
|
||||
},
|
||||
"look-title": "Jetez un coup d'oeil",
|
||||
"website-screenshot-alt": "Une capture d'écran de MyLinks"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"role": "Rôle",
|
||||
"created_at": "Création",
|
||||
"updated_at": "Mise à jour",
|
||||
"admin": "Administrateur",
|
||||
"user": "Utilisateur",
|
||||
"users": "Utilisateurs",
|
||||
"stats": "Statistiques"
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"slogan": "Gérez vos liens de la meilleure des façons",
|
||||
"confirm": "Confirmer",
|
||||
"cancel": "Annuler",
|
||||
"back-home": "← Revenir à l'accueil",
|
||||
"logout": "Déconnexion",
|
||||
"login": "Connexion",
|
||||
"link": {
|
||||
"links": "Liens",
|
||||
"link": "Lien",
|
||||
"name": "Nom du lien",
|
||||
"description": "Description du lien",
|
||||
"create": "Créer un lien",
|
||||
"edit": "Modifier un lien",
|
||||
"remove": "Supprimer un lien",
|
||||
"remove-confirm": "Confirmer la suppression ?"
|
||||
},
|
||||
"category": {
|
||||
"categories": "Catégories",
|
||||
"category": "Catégorie",
|
||||
"name": "Nom de la catégorie",
|
||||
"description": "Description de la catégorie",
|
||||
"visibility": "Public",
|
||||
"no-description": "Aucune description",
|
||||
"create": "Créer une catégorie",
|
||||
"edit": "Modifier une catégorie",
|
||||
"remove": "Supprimer une catégorie",
|
||||
"remove-confirm": "Confirmer la suppression ?",
|
||||
"remove-description": "Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie"
|
||||
},
|
||||
"favorite": "Favoris",
|
||||
"no-item-found": "Aucun élément trouvé",
|
||||
"search": "Rechercher",
|
||||
"avatar": "Avatar de {{name}}",
|
||||
"generic-error": "Une erreur est survenue",
|
||||
"generic-error-description": "Une erreur est survenue, si cela se reproduit merci de <a href=\"https://github.com/Sonny93/my-links\" target=\"_blank\">créer une issue</a> avec le maximum de détails.",
|
||||
"retry": "Recommencer",
|
||||
"privacy": "Confidentialité",
|
||||
"terms": "CGU",
|
||||
"language": {
|
||||
"fr": "Français",
|
||||
"en": "English"
|
||||
},
|
||||
"lang": "Langage",
|
||||
"settings": "Paramètres",
|
||||
"profile": "Profil",
|
||||
"select-your-lang": "Modifier la langue",
|
||||
"name": "Nom",
|
||||
"email": "Email",
|
||||
"footer": {
|
||||
"made_by": "Fait avec ❤\uFE0F par"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"select-category": "Veuillez sélectionner une categories",
|
||||
"or-create-one": "ou en créer une",
|
||||
"no-link": "Aucun lien pour <b>{{name}}</b>"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Authentification",
|
||||
"informative-text": "Authentification requise pour utiliser MyLinks",
|
||||
"continue-with": "Continuer avec {{provider}}"
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"title": "Politique de confidentialité de MyLinks",
|
||||
"edited_at": "Dernière mise à jour : {{date}}",
|
||||
"welcome": "Bienvenue sur MyLinks, un gestionnaire de favoris gratuit et open source axé sur la privacy et le self hosting. Cette politique de confidentialité vise à vous informer sur la manière dont nous collectons, utilisons et protégeons vos données.",
|
||||
"collect": {
|
||||
"title": "1. Collecte de données",
|
||||
"cookie": {
|
||||
"title": "1.1 Cookies",
|
||||
"description": "Les cookies utilisés sur MyLinks sont indispensables pour assurer le bon fonctionnement du site. En continuant à utiliser notre service, vous consentez à l'utilisation de ces cookies."
|
||||
},
|
||||
"user": {
|
||||
"title": "1.2 Données utilisateur",
|
||||
"description": "Pour créer des catégories et liens personnalisés et les associer à leur auteur, nous collectons les informations suivantes :",
|
||||
"fields": [
|
||||
"Identifiant Google",
|
||||
"Nom",
|
||||
"Prénom",
|
||||
"Adresse e-mail",
|
||||
"Avatar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"data_use": {
|
||||
"title": "2. Utilisation des données",
|
||||
"description": "Les données collectées ne sont ni revendues ni utilisées à d'autres fins que celles prévues initialement, à savoir la gestion des catégories et des liens créés par l'utilisateur."
|
||||
},
|
||||
"data_storage": {
|
||||
"title": "3. Stockage des données",
|
||||
"description": "Les données sont stockées de manière sécurisée afin de protéger votre confidentialité.",
|
||||
"data_retention": {
|
||||
"title": "3.1 Durée de conservation",
|
||||
"description": "Les données fonctionnelles sont conservées jusqu'à ce que l'utilisateur fasse une demande de suppression. Une fois cette demande effectuée, les données seront définitivement supprimées."
|
||||
}
|
||||
},
|
||||
"user_rights": {
|
||||
"title": "4. Droits de l'utilisateur",
|
||||
"description": "L'utilisateur a le droit de récupérer l'ensemble de ses données à tout moment et/ou de demander la suppression complète de ses données."
|
||||
},
|
||||
"gdpr": {
|
||||
"title": "5. Conformité au RGPD",
|
||||
"description": "MyLinks est conforme au Règlement Général sur la Protection des Données (RGPD) de l'Union européenne."
|
||||
},
|
||||
"contact": {
|
||||
"title": "6. Contact",
|
||||
"description": "Si vous avez des questions ou des préoccupations concernant notre politique de confidentialité, n'hésitez pas à nous contacter à l'adresse suivante :"
|
||||
},
|
||||
"footer": {
|
||||
"changes": "Nous nous réservons le droit de mettre à jour cette politique de confidentialité. Nous vous encourageons à consulter régulièrement cette page pour rester informé des changements éventuels.",
|
||||
"thanks": "Merci d'utiliser MyLinks !"
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"title": "Conditions Générales d'Utilisation de MyLinks",
|
||||
"edited_at": "Dernière mise à jour : {{date}}",
|
||||
"welcome": "Bienvenue sur MyLinks, un gestionnaire de favoris gratuit et open source axé sur la privacy et le self hosting. En utilisant ce service, vous acceptez les conditions générales d'utilisation énoncées ci-dessous. Veuillez les lire attentivement.",
|
||||
"accept": {
|
||||
"title": "1. Acceptation des Conditions",
|
||||
"description": "En accédant à MyLinks et en utilisant nos services, vous acceptez de vous conformer à ces Conditions Générales d'Utilisation."
|
||||
},
|
||||
"use": {
|
||||
"title": "2. Utilisation du Service",
|
||||
"account": {
|
||||
"title": "2.1 Compte Utilisateur",
|
||||
"description": "Pour accéder à certaines fonctionnalités de MyLinks, vous devrez créer un compte utilisateur. Vous êtes responsable de la confidentialité de votre compte et de vos informations d'identification."
|
||||
},
|
||||
"allowed": {
|
||||
"title": "2.2 Utilisation Autorisée",
|
||||
"description": "Vous vous engagez à utiliser MyLinks conformément aux lois en vigueur et à ne pas violer les droits de tiers."
|
||||
},
|
||||
"user_content": {
|
||||
"title": "2.3 Contenu Utilisateur",
|
||||
"description": "En publiant du contenu sur MyLinks, vous accordez à MyLinks une licence mondiale, non exclusive, transférable et gratuite pour utiliser, reproduire, distribuer et afficher ce contenu."
|
||||
}
|
||||
},
|
||||
"personal_data": {
|
||||
"title": "3. Données Personnelles",
|
||||
"collect": {
|
||||
"title": "3.1 Collecte et Utilisation",
|
||||
"description": "Les données personnelles collectées sont utilisées conformément à notre <a>Politique de Confidentialité</a>. En utilisant MyLinks, vous consentez à cette collecte et utilisation."
|
||||
},
|
||||
"suppress": {
|
||||
"title": "3.2 Suppression de Compte",
|
||||
"description": "Vous pouvez demander la suppression de votre compte à tout moment conformément à notre Politique de Confidentialité."
|
||||
}
|
||||
},
|
||||
"responsibility_warranty": {
|
||||
"title": "4. Responsabilités et Garanties",
|
||||
"responsibility": {
|
||||
"title": "4.1 Responsabilité",
|
||||
"description": "MyLinks ne peut être tenu responsable des dommages directs ou indirects découlant de l'utilisation de nos services."
|
||||
},
|
||||
"warranty": {
|
||||
"title": "4.2 Garanties",
|
||||
"description": "MyLinks ne garantit pas que le service sera exempt d'erreurs ou de interruptions."
|
||||
}
|
||||
},
|
||||
"terms_changes": {
|
||||
"title": "5. Modifications des Conditions",
|
||||
"description": "MyLinks se réserve le droit de modifier ces Conditions Générales\n d'Utilisation à tout moment. Les utilisateurs seront informés des\n changements par le biais d'une notification sur le site."
|
||||
},
|
||||
"cancel": {
|
||||
"title": "6. Résiliation",
|
||||
"description": "MyLinks se réserve le droit de résilier ou de suspendre votre accès au service, avec ou sans préavis, en cas de violation de ces Conditions Générales d'Utilisation."
|
||||
},
|
||||
"contact": {
|
||||
"title": "7. Contact",
|
||||
"description": "Pour toute question ou préoccupation concernant ces Conditions Générales d'Utilisation, veuillez nous contacter à l'adresse suivante :"
|
||||
},
|
||||
"footer": {
|
||||
"changes": "Nous nous réservons le droit de mettre à jour ces Conditions Générales d'Utilisation. Nous vous encourageons à consulter régulièrement cette page pour rester informé des changements éventuels.",
|
||||
"thanks": "Merci d'utiliser MyLinks !"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 500 165" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="tight-bounds" transform="matrix(1.264159, 0, 0, 1.265787, 0, 0)">
|
||||
<svg viewBox="0 0 395.52 130.3536553275838" height="130.3536553275838" width="395.52">
|
||||
<g>
|
||||
<svg viewBox="0 0 609.8756479073379 200.99999999999997" height="130.3536553275838" width="395.52">
|
||||
<g>
|
||||
<rect width="8.33878379307346" height="200.99999999999997" x="222.27426506352492" y="0" stroke-width="0" fill-opacity="1" class="rect-o-0" rx="1%" id="o-0" style="fill: rgb(72, 162, 255);" stroke="transparent"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,252.44232580181685,0.4999999999999858)">
|
||||
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
|
||||
<g>
|
||||
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
|
||||
<g>
|
||||
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<svg width="357.43332210552103" viewBox="3.7404773235321045 -36.008331298828125 142.35955810546875 76.29547882080078" height="200" data-palette-color="#005aa5">
|
||||
<svg/>
|
||||
<svg/>
|
||||
<g>
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M39.3-3.35v0c-0.033 0.967-0.4 1.783-1.1 2.45-0.7 0.667-1.533 0.983-2.5 0.95-0.967-0.033-1.783-0.4-2.45-1.1-0.667-0.7-0.983-1.533-0.95-2.5v0c0.067-2.167 0.033-8.567-0.1-19.2v0c-2.467 3.1-4.9 6.417-7.3 9.95v0c-0.567 0.8-1.317 1.29-2.25 1.47-0.933 0.187-1.8 0.013-2.6-0.52v0c-0.1-0.067-0.183-0.133-0.25-0.2v0c-0.367-0.233-0.683-0.517-0.95-0.85v0l-7.7-9.7c0.033 0.067 0.043 3.257 0.03 9.57-0.02 6.32-0.03 9.497-0.03 9.53v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-1.4 0.007-3.75 0.02-7.05 0.02-3.3 0.03-5.85 0.03-7.65 0-1.8-0.033-4.033-0.1-6.7v0c-0.067-2.367-0.183-4.817-0.35-7.35v0c-0.067-0.967 0.217-1.817 0.85-2.55 0.633-0.733 1.433-1.133 2.4-1.2v0c0.5-0.033 0.967 0.033 1.4 0.2v0c0.933 0.133 1.683 0.583 2.25 1.35v0l11.1 13.95c3.6-5 7.267-9.5 11-13.5v0c0.7-1.2 1.733-1.783 3.1-1.75v0c0.967 0.033 1.783 0.4 2.45 1.1 0.667 0.7 0.983 1.533 0.95 2.5v0c-0.1 3.167-0.1 8.007 0 14.52 0.1 6.52 0.117 11.28 0.05 14.28zM54.6-3.3v0c0-0.833-0.007-1.977-0.02-3.43-0.02-1.447-0.03-2.587-0.03-3.42v0-2.85l-5-8.3c-2.467-4.033-4.3-7.183-5.5-9.45v0c-0.467-0.833-0.567-1.71-0.3-2.63 0.267-0.913 0.827-1.603 1.68-2.07 0.847-0.467 1.73-0.567 2.65-0.3 0.913 0.267 1.603 0.833 2.07 1.7v0c1.1 1.967 3.6 6.183 7.5 12.65v0l2.7-4.2 5.7-8.6c0.5-0.8 1.217-1.317 2.15-1.55 0.933-0.233 1.81-0.093 2.63 0.42 0.813 0.52 1.337 1.247 1.57 2.18 0.233 0.933 0.083 1.817-0.45 2.65v0c-0.2 0.333-2.133 3.267-5.8 8.8v0c-1.967 2.967-3.517 5.45-4.65 7.45v0c0.033 0.767 0.05 2.45 0.05 5.05 0 2.6 0.017 4.5 0.05 5.7v0c0.033 0.967-0.283 1.8-0.95 2.5-0.667 0.7-1.483 1.067-2.45 1.1-0.967 0.033-1.8-0.283-2.5-0.95-0.7-0.667-1.067-1.483-1.1-2.45z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" stroke="none"/>
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M7.9 39.93v0c-1.133-0.033-2.05-0.5-2.75-1.4v0c-0.667-0.667-1.017-1.483-1.05-2.45v0c-0.033-3.4-0.1-8.217-0.2-14.45v0l-0.15-14.05c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0l0.15 13.1 0.15 12.6c1.633 0 4.01-0.017 7.13-0.05 3.113-0.033 5.403-0.05 6.87-0.05v0c0.967 0 1.793 0.323 2.48 0.97 0.68 0.653 1.02 1.447 1.02 2.38 0 0.933-0.34 1.74-1.02 2.42-0.687 0.687-1.513 1.03-2.48 1.03v0c-1.1 0-3.95 0.017-8.55 0.05v0c-4.633 0.033-7.5 0.033-8.6 0zM34.5 39.98v0c-0.967 0-1.8-0.327-2.5-0.98-0.7-0.647-1.067-1.453-1.1-2.42v0c0-0.867 0.277-1.643 0.83-2.33 0.547-0.68 1.237-1.087 2.07-1.22v0c0.033-5.8 0.017-13.15-0.05-22.05v0c-0.8-0.167-1.467-0.567-2-1.2-0.533-0.633-0.8-1.383-0.8-2.25v0c0-0.967 0.35-1.783 1.05-2.45 0.7-0.667 1.533-1 2.5-1v0l5.65 0.05c0.967 0 1.783 0.35 2.45 1.05 0.667 0.7 1 1.533 1 2.5v0c0 0.833-0.273 1.567-0.82 2.2-0.553 0.633-1.247 1.033-2.08 1.2v0c0.1 8.767 0.133 16.033 0.1 21.8v0c0.833 0.133 1.527 0.517 2.08 1.15 0.547 0.633 0.837 1.367 0.87 2.2v0c0 0.967-0.323 1.8-0.97 2.5-0.653 0.7-1.463 1.067-2.43 1.1v0zM76.15 39.63v0c-1.3 0.233-2.383-0.167-3.25-1.2v0c-2.233-2.633-5.05-6.533-8.45-11.7v0c-3.7-5.233-6.317-8.783-7.85-10.65v0c0.033 1.333 0.117 4.29 0.25 8.87 0.133 4.587 0.2 8.397 0.2 11.43v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-2.933-0.093-7.677-0.28-14.23-0.18-6.547-0.203-11.503-0.07-14.87v0c0.033-0.967 0.4-1.783 1.1-2.45 0.7-0.667 1.533-0.983 2.5-0.95v0c0.467 0.033 0.917 0.133 1.35 0.3v0c0.633 0.2 1.183 0.567 1.65 1.1v0c0.567 0.7 1.483 1.807 2.75 3.32 1.267 1.52 2.217 2.663 2.85 3.43v0c1.867 1.933 4.633 5.5 8.3 10.7v0l2.4 3.45c0.1-7.733 0.117-14.017 0.05-18.85v0c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0c0.033 4.533-0.017 14.183-0.15 28.95v0c-0.033 0.933-0.367 1.723-1 2.37-0.633 0.653-1.417 1.013-2.35 1.08zM109.85 38.83l-11.25-14.45-4.2 3.45c0.1 2.7 0.217 5.617 0.35 8.75v0c0.033 0.933-0.273 1.757-0.92 2.47-0.653 0.72-1.453 1.107-2.4 1.16-0.953 0.047-1.787-0.257-2.5-0.91-0.72-0.647-1.113-1.453-1.18-2.42v0c-0.5-10.267-0.683-20.267-0.55-30v0c0.033-0.967 0.393-1.783 1.08-2.45 0.68-0.667 1.503-0.993 2.47-0.98 0.967 0.02 1.783 0.37 2.45 1.05 0.667 0.687 1 1.513 1 2.48v0c-0.067 4.233-0.067 8.217 0 11.95v0c5.233-4.467 10.083-9.217 14.55-14.25v0c0.667-0.7 1.477-1.067 2.43-1.1 0.947-0.033 1.78 0.283 2.5 0.95 0.713 0.667 1.087 1.473 1.12 2.42 0.033 0.953-0.283 1.797-0.95 2.53v0c-3.267 3.733-6.6 7.15-10 10.25v0l11.5 14.8c0.6 0.733 0.833 1.583 0.7 2.55v0c-0.1 0.967-0.523 1.75-1.27 2.35-0.753 0.6-1.613 0.843-2.58 0.73-0.967-0.12-1.75-0.563-2.35-1.33zM132.85 40.28v0c-2.567 0.067-4.883-0.333-6.95-1.2-2.067-0.867-3.7-2.133-4.9-3.8-1.2-1.667-1.8-3.6-1.8-5.8v0c0-1.333 0.383-2.377 1.15-3.13 0.767-0.747 1.633-1.12 2.6-1.12v0c0.967 0 1.75 0.34 2.35 1.02 0.6 0.687 0.9 1.447 0.9 2.28v0c0 0.833 0.05 1.527 0.15 2.08 0.1 0.547 0.35 1.053 0.75 1.52v0c0.767 0.933 2.717 1.4 5.85 1.4v0c1.7 0 3.15-0.35 4.35-1.05 1.2-0.7 1.8-1.7 1.8-3v0c0-0.867-0.7-1.7-2.1-2.5v0c-0.967-0.533-3.117-1.367-6.45-2.5v0c-3.633-1.2-6.367-2.593-8.2-4.18-1.833-1.58-2.75-3.737-2.75-6.47v0c0-2.1 0.6-3.933 1.8-5.5 1.2-1.567 2.777-2.767 4.73-3.6 1.947-0.833 4.02-1.25 6.22-1.25v0c2.267-0.067 4.45 0.3 6.55 1.1 2.1 0.8 3.793 1.94 5.08 3.42 1.28 1.487 1.92 3.163 1.92 5.03v0c0 1-0.367 1.857-1.1 2.57-0.733 0.72-1.583 1.08-2.55 1.08v0c-0.667 0-1.317-0.2-1.95-0.6-0.633-0.4-1.033-0.9-1.2-1.5v0c-0.2-0.8-0.49-1.417-0.87-1.85-0.387-0.433-0.897-0.833-1.53-1.2v0c-0.833-0.467-1.583-0.8-2.25-1-0.667-0.2-1.617-0.3-2.85-0.3v0c-1.467 0-2.673 0.273-3.62 0.82-0.953 0.553-1.43 1.263-1.43 2.13v0c0 1.1 0.543 2 1.63 2.7 1.08 0.7 2.603 1.367 4.57 2v0c3.8 1.267 6.333 2.25 7.6 2.95v0c2.1 1.167 3.583 2.4 4.45 3.7 0.867 1.3 1.3 2.933 1.3 4.9v0c0 2.1-0.633 3.973-1.9 5.62-1.267 1.653-2.923 2.937-4.97 3.85-2.053 0.92-4.18 1.38-6.38 1.38z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" stroke="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0.5)">
|
||||
<svg viewBox="0 0 200.44498811830644 199.99999999999997" height="199.99999999999997" width="200.44498811830644">
|
||||
<g>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0.11100006103515625 100 99.77799987792969" enable-background="new 0 0 100 100" height="199.99999999999997" width="200.44498811830644" class="icon-icon-0" data-fill-palette-color="accent" id="icon-0">
|
||||
<path d="M53.406 36.706L43.25 36.707c0 5.521 4.547 9.999 10.156 9.999 9.07 0 16.592 7.522 16.592 16.592H70c0 9.07-7.522 16.592-16.592 16.592H0l31 19.999v-5c0-2.762 2.238-5 5-5h17.408v0.003C68.07 89.892 80 77.962 80 63.298 80 48.636 68.07 36.706 53.406 36.706z" style="fill: rgb(72, 162, 255);"/>
|
||||
<path d="M46.592 63.294l10.157-0.001c0-5.521-4.548-9.999-10.157-9.999C37.521 53.294 30 45.771 30 36.702c0-9.07 7.522-16.591 16.592-16.591H100l-31-20v5c0 2.762-2.238 5-5 5H46.592v-0.003C31.93 10.108 20 22.038 20 36.702h-0.002C19.998 51.364 31.928 63.294 46.592 63.294z" style="fill: rgb(72, 162, 255);"/>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
<rect width="395.52" height="130.3536553275838" fill="none" stroke="none" visibility="hidden"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.0 KiB |
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "MyLinks",
|
||||
"short_name": "MyLinks",
|
||||
"description": "MyLinks is a free and open source software, that lets you manage your favorite links in an intuitive interface",
|
||||
"launch_handler": {
|
||||
"client_mode": [
|
||||
"focus-existing",
|
||||
"auto"
|
||||
]
|
||||
},
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#f0eef6",
|
||||
"background_color": "#f0eef6",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 44 KiB |