├── .eslintrc.cjs
├── .github
└── workflows
│ ├── dockerhub.yml
│ └── images.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── Dockerfile.api
├── Dockerfile.api.dockerignore
├── Dockerfile.app
├── Dockerfile.app.dockerignore
├── LICENSE.md
├── README.md
├── api
├── .env.example
├── README.md
├── config.example.yml
├── docs
│ ├── docker.md
│ ├── kubernetes.md
│ └── manual.md
├── http
│ ├── auth
│ │ ├── login.http
│ │ └── logout.http
│ ├── code.http
│ ├── codes
│ │ ├── delete.http
│ │ ├── list.http
│ │ ├── make.http
│ │ └── patch.http
│ └── config
│ │ └── patch.http
├── nodemon.json
├── notes
│ └── database.md
├── package-lock.json
├── package.json
├── src
│ ├── app.ts
│ ├── config
│ │ ├── index.ts
│ │ └── interface.ts
│ ├── database
│ │ ├── config.ts
│ │ ├── houseKeeping
│ │ │ ├── createSearchIndex
│ │ │ │ └── index.ts
│ │ │ └── reflectSortedList
│ │ │ │ └── index.ts
│ │ └── index.ts
│ ├── houseKeeping.ts
│ ├── index.ts
│ ├── logger.ts
│ └── server
│ │ ├── cors.ts
│ │ ├── index.ts
│ │ ├── plugins
│ │ └── auth.ts
│ │ ├── routes.ts
│ │ └── routes
│ │ ├── auth
│ │ ├── login
│ │ │ └── index.ts
│ │ └── logout
│ │ │ └── index.ts
│ │ ├── code
│ │ └── index.ts
│ │ ├── codes
│ │ ├── delete
│ │ │ └── index.ts
│ │ ├── list
│ │ │ └── index.ts
│ │ └── make
│ │ │ └── index.ts
│ │ └── config
│ │ ├── index.ts
│ │ └── validate.ts
└── tsconfig.json
├── app
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── src
│ ├── App.tsx
│ ├── components
│ │ ├── CodeCard
│ │ │ ├── CodeCard.tsx
│ │ │ └── index.ts
│ │ ├── CodeModal
│ │ │ ├── CodeModal.tsx
│ │ │ └── index.ts
│ │ ├── Sidebar
│ │ │ └── Sidebar.tsx
│ │ └── Topbar
│ │ │ └── Topbar.tsx
│ ├── index.css
│ ├── index.html
│ ├── index.tsx
│ ├── nprogress.css
│ ├── pages
│ │ ├── Dash
│ │ │ ├── Content.tsx
│ │ │ ├── Dash.tsx
│ │ │ └── codes.ts
│ │ └── Login
│ │ │ ├── Login.tsx
│ │ │ └── index.ts
│ ├── public
│ │ ├── cover.png
│ │ ├── icon.png
│ │ ├── icon.svg
│ │ └── siteicon.svg
│ ├── store
│ │ ├── auth.ts
│ │ ├── codes.ts
│ │ └── index.ts
│ └── util
│ │ ├── hotkeys.ts
│ │ ├── logout.ts
│ │ ├── scrolling.ts
│ │ └── searchAPI.ts
├── tailwind.config.cjs
├── tsconfig.json
└── vite.config.ts
├── docker-compose.yml
├── docs
├── README.md
├── md
│ ├── README.md
│ ├── api
│ │ ├── README.md
│ │ └── docs
│ │ │ ├── docker.md
│ │ │ ├── kubernetes.md
│ │ │ └── manual.md
│ ├── app
│ │ └── README.md
│ └── docs
│ │ └── README.md
├── media
│ ├── cover.png
│ ├── logo_dark.svg
│ └── logo_light.svg
├── notes
│ └── todo.md
├── package-lock.json
├── package.json
├── src
│ ├── build.ts
│ ├── dev.ts
│ ├── helpers
│ │ ├── alpa.ts
│ │ ├── api.ts
│ │ ├── generic.ts
│ │ ├── index.ts
│ │ ├── projects.ts
│ │ └── twitter.ts
│ ├── layout.ts
│ ├── logger.ts
│ └── md.ts
└── tsconfig.json
├── lerna.json
├── package-lock.json
├── package.json
├── prettier.config.cjs
├── tsconfig.base.json
└── tsconfig.json
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /*
2 | * ESLint run control for alpa project.
3 | * Created On 26 April 2022
4 | */
5 |
6 | module.exports = {
7 | plugins: ['prettier', 'simple-import-sort', 'import'],
8 | extends: [
9 | 'eslint:recommended',
10 | 'plugin:prettier/recommended',
11 | 'plugin:react/recommended',
12 | 'plugin:react/jsx-runtime',
13 | ],
14 | env: {
15 | es2021: true,
16 | node: true,
17 | },
18 | parserOptions: {
19 | ecmaVersion: 12,
20 | sourceType: 'module',
21 | ecmaFeatures: {
22 | jsx: true,
23 | },
24 | },
25 | settings: {
26 | 'import/extensions': ['.js'],
27 | react: {
28 | version: '17',
29 | },
30 | },
31 | ignorePatterns: ['**/dist'],
32 | overrides: [
33 | {
34 | files: ['*.ts', '*.tsx'],
35 | parser: '@typescript-eslint/parser',
36 | plugins: [
37 | 'prettier',
38 | 'simple-import-sort',
39 | '@typescript-eslint',
40 | 'import',
41 | ],
42 | extends: [
43 | 'eslint:recommended',
44 | 'plugin:prettier/recommended',
45 | 'plugin:@typescript-eslint/recommended',
46 | 'plugin:react/recommended',
47 | 'plugin:react/jsx-runtime',
48 | ],
49 | rules: {
50 | '@typescript-eslint/no-explicit-any': 'off',
51 | },
52 | },
53 | ],
54 | rules: {
55 | 'linebreak-style': ['error', 'unix'],
56 | quotes: ['off', 'single'],
57 | semi: ['error', 'never'],
58 | 'prettier/prettier': 'error',
59 | 'simple-import-sort/imports': 'error',
60 | 'sort-imports': 'off',
61 | 'import/order': 'off',
62 | 'react/jsx-uses-react': 'error',
63 | 'react/jsx-uses-vars': 'error',
64 | },
65 | }
66 |
--------------------------------------------------------------------------------
/.github/workflows/dockerhub.yml:
--------------------------------------------------------------------------------
1 | name: DockerHub README
2 | on:
3 | workflow_dispatch: {}
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | sync:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Cloning project
12 | uses: actions/checkout@master
13 |
14 | - name: "@alpa/api"
15 | uses: ms-jpq/sync-dockerhub-readme@v1
16 | with:
17 | username: ${{ secrets.DOCKER_USERNAME }}
18 | password: ${{ secrets.DOCKER_PASSWORD }}
19 | repository: vsnthdev/alpa-api
20 | readme: ./api/README.md
21 |
22 | - name: "@alpa/app"
23 | uses: ms-jpq/sync-dockerhub-readme@v1
24 | with:
25 | username: ${{ secrets.DOCKER_USERNAME }}
26 | password: ${{ secrets.DOCKER_PASSWORD }}
27 | repository: vsnthdev/alpa-app
28 | readme: ./app/README.md
29 |
--------------------------------------------------------------------------------
/.github/workflows/images.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Keeps the images on DockerHub updated, by automatically building
3 | # a new Docker image, and pushing it to DockerHub.
4 | # Created On 19 February 2022
5 | #
6 |
7 | name: Docker Images
8 | on:
9 | workflow_dispatch: {}
10 | push:
11 | branches:
12 | - main
13 | jobs:
14 | api:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Cloning project
18 | uses: actions/checkout@master
19 |
20 | - name: Setting up Node.js
21 | uses: actions/setup-node@v2
22 | with:
23 | node-version: "17.4.0"
24 |
25 | - name: Installing dependencies
26 | run: npm install --dev
27 |
28 | - name: Building API
29 | run: npm run build
30 |
31 | - name: Reading API version
32 | id: package-version
33 | uses: martinbeentjes/npm-get-version-action@master
34 | with:
35 | path: api
36 |
37 | - name: Creating Docker image metadata
38 | id: meta
39 | uses: docker/metadata-action@v3
40 | with:
41 | # list of Docker images to use as base name for tags
42 | images: |
43 | vsnthdev/alpa-api
44 |
45 | - name: Setting up QEMU
46 | uses: docker/setup-qemu-action@v1
47 |
48 | - name: Setting up Docker Buildx
49 | uses: docker/setup-buildx-action@v1
50 |
51 | - name: Logging in to DockerHub
52 | uses: docker/login-action@v1
53 | with:
54 | username: ${{ secrets.DOCKER_USERNAME }}
55 | password: ${{ secrets.DOCKER_TOKEN }}
56 |
57 | - name: Building & pushing to DockerHub
58 | uses: docker/build-push-action@v2
59 | with:
60 | push: true
61 | context: .
62 | file: Dockerfile.api
63 | tags: "vsnthdev/alpa-api:latest,vsnthdev/alpa-api:v${{ steps.package-version.outputs.current-version}}"
64 | labels: ${{ steps.meta.outputs.labels }}
65 |
66 | app:
67 | runs-on: ubuntu-latest
68 | steps:
69 | - name: Cloning project
70 | uses: actions/checkout@master
71 |
72 | - name: Setting up Node.js
73 | uses: actions/setup-node@v2
74 | with:
75 | node-version: "17.4.0"
76 |
77 | - name: Installing dependencies
78 | run: npm install --dev
79 |
80 | - name: Building the app
81 | run: npm run build
82 |
83 | - name: Reading the app version
84 | id: package-version
85 | uses: martinbeentjes/npm-get-version-action@master
86 | with:
87 | path: app
88 |
89 | - name: Creating Docker image metadata
90 | id: meta
91 | uses: docker/metadata-action@v3
92 | with:
93 | # list of Docker images to use as base name for tags
94 | images: |
95 | vsnthdev/alpa-app
96 |
97 | - name: Setting up QEMU
98 | uses: docker/setup-qemu-action@v1
99 |
100 | - name: Setting up Docker Buildx
101 | uses: docker/setup-buildx-action@v1
102 |
103 | - name: Logging in to DockerHub
104 | uses: docker/login-action@v1
105 | with:
106 | username: ${{ secrets.DOCKER_USERNAME }}
107 | password: ${{ secrets.DOCKER_TOKEN }}
108 |
109 | - name: Building & pushing
110 | uses: docker/build-push-action@v2
111 | with:
112 | push: true
113 | context: .
114 | file: Dockerfile.app
115 | tags: "vsnthdev/alpa-app:latest,vsnthdev/alpa-app:v${{ steps.package-version.outputs.current-version}}"
116 | labels: ${{ steps.meta.outputs.labels }}
117 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/node,visualstudiocode
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,visualstudiocode
3 |
4 | ### Node ###
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 | .pnpm-debug.log*
13 |
14 | # Diagnostic reports (https://nodejs.org/api/report.html)
15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | # Directory for instrumented libs generated by jscoverage/JSCover
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 | coverage
28 | *.lcov
29 |
30 | # nyc test coverage
31 | .nyc_output
32 |
33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34 | .grunt
35 |
36 | # Bower dependency directory (https://bower.io/)
37 | bower_components
38 |
39 | # node-waf configuration
40 | .lock-wscript
41 |
42 | # Compiled binary addons (https://nodejs.org/api/addons.html)
43 | build/Release
44 |
45 | # Dependency directories
46 | node_modules/
47 | jspm_packages/
48 |
49 | # Snowpack dependency directory (https://snowpack.dev/)
50 | web_modules/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Optional stylelint cache
62 | .stylelintcache
63 |
64 | # Microbundle cache
65 | .rpt2_cache/
66 | .rts2_cache_cjs/
67 | .rts2_cache_es/
68 | .rts2_cache_umd/
69 |
70 | # Optional REPL history
71 | .node_repl_history
72 |
73 | # Output of 'npm pack'
74 | *.tgz
75 |
76 | # Yarn Integrity file
77 | .yarn-integrity
78 |
79 | # dotenv environment variable files
80 | .env
81 | .env.development.local
82 | .env.test.local
83 | .env.production.local
84 | .env.local
85 |
86 | # parcel-bundler cache (https://parceljs.org/)
87 | .cache
88 | .parcel-cache
89 |
90 | # Next.js build output
91 | .next
92 | out
93 |
94 | # Nuxt.js build / generate output
95 | .nuxt
96 | dist
97 |
98 | # Gatsby files
99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and not Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 |
104 | # vuepress build output
105 | .vuepress/dist
106 |
107 | # vuepress v2.x temp and cache directory
108 | .temp
109 |
110 | # Docusaurus cache and generated files
111 | .docusaurus
112 |
113 | # Serverless directories
114 | .serverless/
115 |
116 | # FuseBox cache
117 | .fusebox/
118 |
119 | # DynamoDB Local files
120 | .dynamodb/
121 |
122 | # TernJS port file
123 | .tern-port
124 |
125 | # Stores VSCode versions used for testing VSCode extensions
126 | .vscode-test
127 |
128 | # yarn v2
129 | .yarn/cache
130 | .yarn/unplugged
131 | .yarn/build-state.yml
132 | .yarn/install-state.gz
133 | .pnp.*
134 |
135 | ### Node Patch ###
136 | # Serverless Webpack directories
137 | .webpack/
138 |
139 | # Optional stylelint cache
140 |
141 | # SvelteKit build / generate output
142 | .svelte-kit
143 |
144 | ### VisualStudioCode ###
145 | .vscode/*
146 | !.vscode/settings.json
147 | !.vscode/tasks.json
148 | !.vscode/launch.json
149 | !.vscode/extensions.json
150 | !.vscode/*.code-snippets
151 |
152 | # Local History for Visual Studio Code
153 | .history/
154 |
155 | # Built Visual Studio Code Extensions
156 | *.vsix
157 |
158 | ### VisualStudioCode Patch ###
159 | # Ignore all local history of files
160 | .history
161 | .ionide
162 |
163 | # Support for Project snippet scope
164 |
165 | # End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode
166 |
167 | www/
168 | api/config.yml
169 | .redis
170 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run --silent lint
5 | npm run --silent build:docs
6 | git add --all
7 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | #
2 | # NPM run control for alpa project.
3 | # Created On 31 March 2022
4 | #
5 |
6 | # only allow Node.js version specified
7 | # in package.json file
8 | engine-strict=true
9 |
--------------------------------------------------------------------------------
/Dockerfile.api:
--------------------------------------------------------------------------------
1 | #
2 | # Instructions to build vsnthdev/alpa-api docker image.
3 | # Created On 18 February 2022
4 | #
5 |
6 | # This image only contains the API, an image of the
7 | # frontend can be found at vsnthdev/alpa-app.
8 |
9 | # small & updated base image
10 | FROM node:17.8.0-alpine3.15
11 |
12 | # run Node.js in production so the API
13 | # can take the necessary security measures
14 | ENV NODE_ENV=production
15 |
16 | # where the API source code will be
17 | WORKDIR /opt/alpa
18 |
19 | # copy this directory to the image
20 | COPY . /opt/alpa
21 |
22 | # install dependencies
23 | RUN npm install --prod && \
24 | rm -rf /var/cache/apk/*
25 |
26 | # run @alpa/api on contianer boot
27 | CMD node api/dist/index.js --verbose
28 |
--------------------------------------------------------------------------------
/Dockerfile.api.dockerignore:
--------------------------------------------------------------------------------
1 | app
2 | **/Dockerfile*
3 | **/node_modules
4 | **/tsconfig*
5 | **/.git*
6 | api/config.yml
7 | api/http
8 | api/nodemon*
9 | api/notes
10 | api/src
11 | docker-compose.yml
12 |
--------------------------------------------------------------------------------
/Dockerfile.app:
--------------------------------------------------------------------------------
1 | #
2 | # Instructions to build vsnthdev/alpa-app docker image.
3 | # Created On 18 February 2022
4 | #
5 |
6 | # This image only contains the frontend app, an image of the
7 | # API can be found at vsnthdev/alpa-api.
8 |
9 | # small & updated base image
10 | FROM node:17.8.0-alpine3.15
11 |
12 | # run Node.js in production
13 | ENV NODE_ENV=production
14 |
15 | # where the API source code will be
16 | WORKDIR /opt/alpa
17 |
18 | # copy this directory to the image
19 | COPY . /opt/alpa
20 |
21 | # install sirv static file server
22 | RUN npm install --global sirv-cli && \
23 | rm -rf /var/cache/apk/*
24 |
25 | # run @alpa/app using sirv on contianer boot
26 | CMD cd app/dist && sirv . --port 3000 --host "0.0.0.0" --single --no-clear
27 |
--------------------------------------------------------------------------------
/Dockerfile.app.dockerignore:
--------------------------------------------------------------------------------
1 | api
2 | **/Dockerfile*
3 | **/node_modules
4 | **/tsconfig*
5 | **/.git*
6 | app/vite*
7 | app/tailwind*
8 | app/src
9 | api/README.md
10 | api/.env.example
11 | docker-compose.yml
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
( अल्प ) — A fast ⚡ self-hosted link 🔗 shortener.
14 | 15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
( अल्प ) — A fast ⚡ self-hosted link 🔗 shortener.
14 | 15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
( अल्प ) — A fast ⚡ self-hosted link 🔗 shortener.
14 | 15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
( अल्प ) — A fast ⚡ self-hosted link 🔗 shortener.
14 | 15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
( अल्प ) — A fast ⚡ self-hosted link 🔗 shortener.
14 | 15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
=> {
23 | const codes: any[] = []
24 |
25 | for (const key of keys) {
26 | const code = await db.codes.json.get(key)
27 | codes.push({ ...{ code: key }, ...code })
28 | }
29 |
30 | return codes
31 | }
32 |
33 | const documentsToCodes = async (docs: any[]) => {
34 | const codes: any[] = []
35 |
36 | for (const doc of docs) {
37 | doc.value['code'] = doc.id
38 | codes.push(doc.value)
39 | }
40 |
41 | return codes
42 | }
43 |
44 | const getRecentList = async (query: RequestQuery): Promise => {
45 | // get the number of total keys in the database
46 | const total: number = await db.codes.dbSize()
47 |
48 | // create a response skeleton object
49 | const res: ResponseImpl = {
50 | pages: -1,
51 | codes: [],
52 | }
53 |
54 | // handle when there are no codes in the database
55 | if (total == 0) return { ...res, ...{ pages: 0 } }
56 |
57 | // initialize the cursor variable
58 | if (typeof query.page != 'string') query.page = '0'
59 |
60 | // now fetch keys from our sorted set in Redis
61 | const count = 10
62 | const start = count * parseInt(query.page)
63 | const end = start + (count - 1)
64 |
65 | const keys = await db.config.zRange('codes', start, end, {
66 | REV: true,
67 | })
68 |
69 | // convert database keys to actual codes
70 | const codes = await keysToCodes(keys)
71 |
72 | return { ...res, ...{ pages: Math.round(total / count), codes } }
73 | }
74 |
75 | const executeQuery = async ({ search }: RequestQuery) => {
76 | // remove any special characters since
77 | // that crashes the server
78 | search = search.replace(/[^a-zA-Z0-9 ]/g, '')
79 |
80 | // the result
81 | const results = { codes: [] }
82 |
83 | // don't perform a search, if input is nothing
84 | if (!search) return results
85 |
86 | // search for direct keys
87 | const { keys } = await db.codes.scan(0, {
88 | MATCH: `"${search}*"`,
89 | })
90 |
91 | results.codes = results.codes.concat((await keysToCodes(keys)) as any)
92 |
93 | // search for tags
94 | const { documents } = await db.codes.ft.search(
95 | 'codes',
96 | `@tags:{ ${search
97 | .split(' ')
98 | .map(tag => `${tag}*`)
99 | .join(' | ')
100 | .trim()} }`,
101 | )
102 |
103 | results.codes = results.codes.concat(
104 | (await documentsToCodes(documents)) as any,
105 | )
106 |
107 | return results
108 | }
109 |
110 | const handler = async (req: FastifyRequest, rep: FastifyReply) => {
111 | const query = req.query as RequestQuery
112 | const toSend = rep.status(200)
113 |
114 | if (query.search) {
115 | return toSend.send(await executeQuery(query))
116 | } else {
117 | return toSend.send(await getRecentList(query))
118 | }
119 | }
120 |
121 | export default {
122 | handler,
123 | method: 'GET',
124 | url: ['/api/codes'],
125 | preValidation: [auth],
126 | }
127 |
--------------------------------------------------------------------------------
/api/src/server/routes/codes/make/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Creates a new short code.
3 | * Created On 03 February 2022
4 | */
5 |
6 | import boom from 'boom'
7 | import { FastifyReply, FastifyRequest } from 'fastify'
8 |
9 | import { db } from '../../../../database/index.js'
10 | import auth from '../../../plugins/auth.js'
11 |
12 | export interface CodeLink {
13 | title: string
14 | icon: string
15 | image: string
16 | url: string
17 | }
18 |
19 | export interface Code {
20 | code?: string
21 | tags: string
22 | links: CodeLink[]
23 | }
24 |
25 | const handler = async (req: FastifyRequest, rep: FastifyReply) => {
26 | const body = req.body as Code
27 | const code = body.code
28 | const query = req.query as any
29 | delete body.code
30 |
31 | if (code == 'api')
32 | throw boom.notAcceptable('A code named api cannot be created.')
33 |
34 | const exists = await db.codes.exists(code)
35 | if (exists && Boolean(query['force']) == false)
36 | throw boom.conflict('That code already exists')
37 |
38 | await db.codes.json.set(code, '$', body)
39 |
40 | if (exists) {
41 | return rep.status(200).send({
42 | message: 'Updated the code',
43 | })
44 | } else {
45 | // fetch the last value in sorted list and it's score
46 | let lastScore: number
47 | try {
48 | const [last] = await db.config.zRangeWithScores('codes', 0, 0, {
49 | REV: true,
50 | })
51 |
52 | lastScore = last.score
53 | } catch {
54 | lastScore = 0
55 | }
56 |
57 | // add the newly created code to our sorted set
58 | await db.config.zAdd('codes', { score: lastScore + 1, value: code })
59 |
60 | return rep.status(201).send({
61 | message: 'Created a new code',
62 | })
63 | }
64 | }
65 |
66 | export default {
67 | handler,
68 | method: 'POST',
69 | url: ['/api/codes'],
70 | preValidation: [auth],
71 | }
72 |
--------------------------------------------------------------------------------
/api/src/server/routes/config/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Creates, updates an existing, or deletes configuration.
3 | * Created On 10 May 2022
4 | */
5 |
6 | import boom from 'boom'
7 | import { FastifyReply, FastifyRequest } from 'fastify'
8 |
9 | import config from '../../../database/config.js'
10 | import auth from '../../plugins/auth.js'
11 | import validate from './validate.js'
12 |
13 | export interface ResponseImpl {
14 | message: string
15 | data?: any
16 | }
17 |
18 | const ajvErrorResponseTransform = (func: any, err: any) => {
19 | err.output.payload['data'] = func.errors?.map((e: any) => {
20 | delete e.schemaPath
21 | return e
22 | })
23 | throw err
24 | }
25 |
26 | const handler = async (req: FastifyRequest, rep: FastifyReply) => {
27 | // validate user input
28 | if (!validate(req.body as any)) {
29 | const err = boom.badRequest('Invalid config request')
30 | ajvErrorResponseTransform(validate, err)
31 | }
32 |
33 | // write the changes to the database
34 | await config.set(req.body)
35 |
36 | return rep.status(201).send({
37 | message: 'Updated config accordingly',
38 | })
39 | }
40 |
41 | export default {
42 | handler,
43 | method: 'POST',
44 | url: ['/api/config'],
45 | preValidation: [auth],
46 | }
47 |
--------------------------------------------------------------------------------
/api/src/server/routes/config/validate.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Validates the user input to only accept allowed config keys.
3 | * Created On 11 May 2022
4 | */
5 |
6 | import Ajv, { JSONSchemaType } from 'ajv'
7 |
8 | export interface Schema {
9 | server?: {
10 | host?: string
11 | port?: number
12 | cors?: string[]
13 | }
14 | }
15 |
16 | const schema: JSONSchemaType = {
17 | type: 'object',
18 | // additionalProperties: false,
19 | properties: {
20 | server: {
21 | type: 'object',
22 | nullable: true,
23 | additionalProperties: false,
24 | properties: {
25 | host: {
26 | type: 'string',
27 | nullable: true,
28 | },
29 | port: {
30 | type: 'number',
31 | nullable: true,
32 | },
33 | cors: {
34 | type: 'array',
35 | nullable: true,
36 | minItems: 1,
37 | items: {
38 | type: 'string',
39 | minLength: 4,
40 | },
41 | },
42 | },
43 | },
44 | },
45 | }
46 |
47 | export default new Ajv().compile(schema)
48 |
--------------------------------------------------------------------------------
/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "strictNullChecks": true,
5 | "rootDir": "src",
6 | "outDir": "dist"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | .vercel
2 |
--------------------------------------------------------------------------------
/app/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ( अल्प ) — A fast ⚡ self-hosted link 🔗 shortener.
14 |
15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | This project contains a friendly dashboard deployed at https://alpa.vercel.app which can be used to control **alpa's API hosted anywhere**.
34 |
35 | ## 🔮 Tech stack
36 |
37 | | Name | Description |
38 | | --- | --- |
39 | |
**React.js** | Frontend framework of choice. |
40 | |
**Redux** | Store management for React. |
41 | |
**TailwindCSS** | CSS framework for rapid UI building. |
42 | |
**Vite.js** | For bundling JavaScript. |
43 | |
**Vercel** | For deploying frontend. |
44 | |
**nanoid** | For creating short codes. |
45 |
46 | ## 💻 Building & Dev Setup
47 |
48 | You need to be at least on **Node.js v17.4.0 or above** and follow the below instructions to build this project 👇
49 |
50 | - **STEP 1️⃣** Clone this repository & enter into it (`cd ./alpa`)
51 | - **STEP 2️⃣** Run **`npm install`** to get all dependencies & link projects together
52 | - **STEP 3️⃣** Enter in the project directory (`cd app`)
53 | - **STEP 4️⃣** To build this project run **`npm run build`**
54 |
55 | Upon building `@alpa/app` a production optimized bundle of React.js app is generated in the `dist` folder within the project.
56 |
57 | ## 📰 License
58 | > The **alpa** project is released under the [AGPL-3.0-only](https://github.com/vsnthdev/alpa/blob/main/LICENSE.md).
Developed & maintained By Vasanth Srivatsa. Copyright 2022 © Vasanth Developer.
59 |
60 |
61 | > vsnth.dev ·
62 | > YouTube @vasanthdeveloper ·
63 | > Twitter @vsnthdev ·
64 | > Discord Vasanth Developer
65 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@alpa/app",
3 | "description": "Dashboard ✨ to interact with alpa's API.",
4 | "version": "1.1.1",
5 | "type": "module",
6 | "scripts": {
7 | "clean": "rimraf dist tsconfig.tsbuildinfo",
8 | "start": "vite",
9 | "build": "vite build --emptyOutDir",
10 | "preview": "vite build --emptyOutDir && sirv dist --port 5000",
11 | "build:docker": "cd .. && docker build . --tag vsnthdev/alpa-app:latest -f Dockerfile.app",
12 | "vercel": "vite build --emptyOutDir && vercel --prod && rimraf dist tsconfig.tsbuildinfo"
13 | },
14 | "devDependencies": {
15 | "@reduxjs/toolkit": "^1.7.2",
16 | "@tippyjs/react": "^4.2.6",
17 | "@types/nprogress": "^0.2.0",
18 | "@types/react": "^17.0.39",
19 | "@types/react-dom": "^17.0.11",
20 | "@types/react-tag-input": "^6.1.3",
21 | "@types/tailwindcss": "^3.0.7",
22 | "@vitejs/plugin-react": "^1.2.0",
23 | "autoprefixer": "^10.4.2",
24 | "is-mobile": "^3.0.0",
25 | "nanoid": "^3.3.0",
26 | "nprogress": "^0.2.0",
27 | "postcss": "^8.4.6",
28 | "react-dnd": "^14.0.5",
29 | "react-dnd-html5-backend": "^14.1.0",
30 | "react-hotkeys": "^2.0.0",
31 | "react-redux": "^7.2.6",
32 | "react-router-dom": "^6.2.1",
33 | "react-tag-input": "^6.8.0",
34 | "sirv-cli": "^2.0.2",
35 | "tailwindcss": "^3.0.19",
36 | "use-debounce": "^7.0.1",
37 | "vercel": "^24.0.0",
38 | "vite": "^2.7.13",
39 | "vite-plugin-html": "^3.0.3",
40 | "vite-plugin-pwa": "^0.11.13"
41 | },
42 | "dependencies": {
43 | "react": "^17.0.2",
44 | "react-dom": "^17.0.2"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/App.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * The App shell that encapsulating the entire application.
3 | * Created On 08 February 2022
4 | */
5 |
6 | import { ReactElement, StrictMode, useState } from 'react'
7 | import { HotKeys } from 'react-hotkeys'
8 | import { Provider } from 'react-redux'
9 | import { BrowserRouter, Route, Routes } from 'react-router-dom'
10 |
11 | import { prepareModalState } from './components/CodeModal'
12 | import { Sidebar } from './components/Sidebar/Sidebar'
13 | import { Topbar } from './components/Topbar/Topbar'
14 | import { Dash } from './pages/Dash/Dash'
15 | import { Login } from './pages/Login/Login'
16 | import { store } from './store/index.js'
17 | import { hotkeyHandlers, hotkeyMap } from './util/hotkeys'
18 |
19 | export const App = (): ReactElement => {
20 | const [loading, setLoading] = useState(true)
21 | const [quickText, setQuickText] = useState('')
22 |
23 | // prepare modal's required state
24 | const modalState = prepareModalState()
25 |
26 | return (
27 |
28 |
29 |
30 |
35 | {/* the sidebar */}
36 |
37 |
38 | {/* the routes link to their pages */}
39 |
40 | {/* the topbar */}
41 |
47 |
48 |
49 | }
52 | >
53 |
63 | }
64 | >
65 |
66 |
67 |
68 |
69 |
70 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/components/CodeCard/CodeCard.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * A single code block, which can be edited or modified.
3 | * Created On 11 February 2022
4 | */
5 |
6 | import Tippy from '@tippyjs/react'
7 | import { Dispatch, ReactElement, useState } from 'react'
8 | import { useDispatch, useSelector } from 'react-redux'
9 |
10 | import { AppState } from '../../store'
11 | import { Code } from '../../store/codes'
12 | import { searchAPI } from '../../util/searchAPI'
13 | import { CodeModalStateReturns, openCodeModal } from '../CodeModal'
14 | import { copyShortURL, del, getColorFromTag } from './index'
15 |
16 | export const CodeCard = ({
17 | code,
18 | modalState,
19 | quickText,
20 | setQuickText,
21 | }: {
22 | code: Code
23 | quickText: string
24 | modalState: CodeModalStateReturns
25 | setQuickText: Dispatch>
26 | }): ReactElement => {
27 | const [showCopiedTooltip, setShowCopiedToolTip] = useState(false)
28 | const auth = useSelector((state: AppState) => state.auth)
29 | const dispatch = useDispatch()
30 |
31 | return (
32 |
33 | {/* the code of the item */}
34 |
35 |
36 | {code.code}
37 |
38 |
39 |
40 | {/* the link */}
41 |
42 |
48 | {code.links[0].url}
49 |
50 |
51 |
52 | {/* tags & card actions */}
53 |
54 | {/* render tags if exist */}
55 | {code.tags ? (
56 |
57 | {code.tags.split(';').map(tag => (
58 | {
61 | setQuickText(tag)
62 | searchAPI({
63 | auth,
64 | dispatch,
65 | quickText,
66 | })
67 | }}
68 | className="cursor-pointer text-black px-3 py-1 mr-2 mb-2 rounded-full lg:mb-0"
69 | style={{
70 | backgroundColor: getColorFromTag(tag),
71 | }}
72 | >
73 | {tag}
74 |
75 | ))}
76 |
77 | ) : (
78 |
79 | Not tagged
80 |
81 | )}
82 |
83 | {/* the card actions */}
84 |
85 | {/* copy short URL button */}
86 | {Boolean(navigator.clipboard) && (
87 |
92 |
95 | setShowCopiedToolTip(false)
96 | }
97 | content="✅ Copied short URL"
98 | theme="primary"
99 | animation="shift-away"
100 | inertia={true}
101 | >
102 |
127 |
128 |
129 | )}
130 |
131 | {/* edit button */}
132 |
133 |
154 |
155 |
156 | {/* delete button */}
157 |
162 |
188 |
189 |
190 |
191 |
192 | )
193 | }
194 |
--------------------------------------------------------------------------------
/app/src/components/CodeCard/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Contains additional importable functions to work with short codes.
3 | * Created On 12 February 2022
4 | */
5 |
6 | import { Dispatch } from '@reduxjs/toolkit'
7 | import axios from 'axios'
8 |
9 | import { Code, del as _del } from '../../store/codes'
10 |
11 | export const del = ({
12 | apiHost,
13 | apiToken,
14 | code,
15 | dispatch,
16 | }: {
17 | apiHost: string
18 | apiToken: string
19 | code: string
20 | dispatch: Dispatch
21 | }) =>
22 | axios({
23 | method: 'DELETE',
24 | url: `${apiHost}/api/codes/${code}`,
25 | headers: {
26 | Authorization: `Bearer ${apiToken}`,
27 | },
28 | }).then(() => {
29 | // update our application state
30 | dispatch(_del(code))
31 | })
32 |
33 | export const copyShortURL = ({
34 | code,
35 | apiHost,
36 | setShowCopiedToolTip,
37 | }: {
38 | code: Code
39 | apiHost: string
40 | setShowCopiedToolTip: React.Dispatch>
41 | }) =>
42 | navigator.clipboard.writeText(`${apiHost}/${code.code}`).then(() => {
43 | setShowCopiedToolTip(true)
44 | setTimeout(() => setShowCopiedToolTip(false), 1000)
45 | })
46 |
47 | // copied from 👇
48 | // https://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-javascript
49 | export const getColorFromTag = (tag: string) => {
50 | const stringUniqueHash = [...tag].reduce((acc, char) => {
51 | return char.charCodeAt(0) + ((acc << 5) - acc)
52 | }, 0)
53 |
54 | return `hsla(${stringUniqueHash % 360}, 95%, 35%, 0.15)`
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/components/CodeModal/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * This file contains functions, to prepare and handle the state
3 | * of the CodeModal component. But these functions should be invoked
4 | * in the parent component. Not in CodeModal itself.
5 | * Created On 13 February 2022
6 | */
7 |
8 | import axios from 'axios'
9 | import isMobile from 'is-mobile'
10 | import { customAlphabet } from 'nanoid'
11 | import { Dispatch, useState } from 'react'
12 |
13 | import { AuthState } from '../../store/auth'
14 | import { Code, patch } from '../../store/codes'
15 |
16 | const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 4)
17 |
18 | export interface CodeModalStateReturns {
19 | code: Code
20 | isOpen: boolean
21 | isCreatingNew: boolean
22 | setCode: React.Dispatch>
23 | setIsOpen: React.Dispatch>
24 | setIsCreatingNew: React.Dispatch>
25 | }
26 |
27 | export const prepareModalState = (): CodeModalStateReturns => {
28 | const [isOpen, setIsOpen] = useState(false)
29 | const [isCreatingNew, setIsCreatingNew] = useState(false)
30 |
31 | const [code, setCode] = useState({
32 | code: '',
33 | links: [
34 | {
35 | url: '',
36 | },
37 | ],
38 | tags: '',
39 | } as Code)
40 |
41 | return { code, setCode, isOpen, setIsOpen, isCreatingNew, setIsCreatingNew }
42 | }
43 |
44 | export const openCodeModal = (
45 | code: Code | null,
46 | state: CodeModalStateReturns,
47 | ) => {
48 | // function to focus on our target input field
49 | const focus = () =>
50 | (document.querySelector('#target') as HTMLInputElement).focus()
51 |
52 | // set the code if we're editing
53 | // an existing one, or else set as a new code dialog
54 | if (code) {
55 | state.setIsCreatingNew(false)
56 | state.setCode({
57 | ...code,
58 | ...{
59 | tags: code.tags
60 | .split(';')
61 | .map(tag => tag.trim())
62 | .filter(tag => Boolean(tag))
63 | .join('; '),
64 | },
65 | } as Code)
66 | } else {
67 | state.setCode({
68 | ...state.code,
69 | ...{ code: generateCodeString() },
70 | } as Code)
71 | state.setIsCreatingNew(true)
72 | }
73 |
74 | // show the modal & set focus on target
75 | state.setIsOpen(true)
76 | isMobile() ? (document.activeElement as HTMLElement).blur() : focus()
77 | }
78 |
79 | const closeModal = (state: CodeModalStateReturns) => state.setIsOpen(false)
80 |
81 | const clearState = (state: CodeModalStateReturns) =>
82 | state.setCode({
83 | code: '',
84 | links: [{ url: '' }],
85 | tags: '',
86 | })
87 |
88 | export const cancelAction = (state: CodeModalStateReturns) => {
89 | closeModal(state)
90 | clearState(state)
91 | ;(document.querySelector('#btnNew') as any).focus()
92 | }
93 |
94 | export const applyAction = (
95 | state: CodeModalStateReturns,
96 | dispatch: Dispatch,
97 | auth: AuthState,
98 | ) => {
99 | closeModal(state)
100 |
101 | // prepare a final new Code object
102 | const getTags = (tags: string) =>
103 | tags
104 | .split(';')
105 | .map(tag => tag.trim())
106 | .filter(tag => tag.length > 0)
107 | .join(';')
108 | const final = { ...state.code, ...{ tags: getTags(state.code.tags) } }
109 |
110 | // send HTTP request
111 | axios({
112 | method: 'POST',
113 | url: `${auth.apiHost}/api/codes?force=true`,
114 | headers: {
115 | Authorization: `Bearer ${auth.apiToken}`,
116 | },
117 | data: final,
118 | }).then(() => {
119 | // dispatch a app store change
120 | dispatch(patch(final))
121 | })
122 |
123 | clearState(state)
124 | }
125 |
126 | export const generateCodeString = (): string => nanoid()
127 |
--------------------------------------------------------------------------------
/app/src/components/Topbar/Topbar.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Site-wide header component contains mainly the
3 | * logo & user profile menu.
4 | * Created On 08 February 2022
5 | */
6 |
7 | import { Dispatch, ReactElement } from 'react'
8 | import { useDispatch, useSelector } from 'react-redux'
9 | import { useNavigate } from 'react-router-dom'
10 | import { useDebouncedCallback } from 'use-debounce'
11 |
12 | import { AppState } from '../../store/index'
13 | import logout from '../../util/logout'
14 | import { searchAPI } from '../../util/searchAPI'
15 |
16 | export const Topbar = ({
17 | loading,
18 | quickText,
19 | setQuickText,
20 | setLoading,
21 | }: {
22 | loading: boolean
23 | quickText: string
24 | setQuickText: Dispatch>
25 | setLoading: React.Dispatch>
26 | }): ReactElement => {
27 | const navigate = useNavigate()
28 | const dispatch = useDispatch()
29 |
30 | const auth = useSelector((state: AppState) => state.auth)
31 |
32 | const triggerSearchAPI = useDebouncedCallback(async () => {
33 | if (Boolean(quickText) == false) return
34 |
35 | // call search api api
36 | searchAPI({
37 | auth,
38 | quickText,
39 | dispatch,
40 | })
41 | }, 200)
42 |
43 | return (
44 |
45 |
46 | {/* search bar */}
47 |
48 | {auth.isLoggedIn && loading == false && (
49 | {
56 | setQuickText(e.target.value)
57 | triggerSearchAPI()
58 | }}
59 | />
60 | )}
61 |
62 |
63 | {/* logout button */}
64 | {auth.isLoggedIn ? (
65 |
67 | logout({ auth, navigate, dispatch, setLoading })
68 | }
69 | >
70 |
86 |
87 | ) : (
88 | ''
89 | )}
90 |
91 |
92 | )
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Entry Cascading Stylesheet file. Contains mainly TailwindCSS imports.
3 | * Created On 08 February 2022
4 | */
5 |
6 | @tailwind base;
7 | @tailwind components;
8 | @tailwind utilities;
9 |
10 | /* hide scrollbar */
11 |
12 | ::-webkit-scrollbar {
13 | display: none;
14 | }
15 |
16 | /* prevent blue color highlight on clicking in mobile devices */
17 |
18 | * {
19 | -webkit-tap-highlight-color: transparent;
20 | }
21 |
22 | /* prevent black outline on focus */
23 |
24 | * {
25 | outline: none !important;
26 | }
27 |
28 | /* Tippy.js styles */
29 |
30 | .tippy-box[data-theme~='primary'] {
31 | @apply bg-primary border-2 border-solid border-primary font-medium;
32 | }
33 |
34 | .tippy-box[data-theme~='primary'] .tippy-arrow {
35 | @apply text-primary;
36 | }
37 |
38 | .tippy-box[data-theme~='light'] {
39 | @apply bg-white text-neutral-900 border-2 border-solid border-neutral-300 font-medium;
40 | }
41 |
42 | .tippy-box[data-theme~='light'] .tippy-arrow {
43 | @apply text-white;
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | alpa — A fast ⚡ self-hosted link 🔗 shortener.
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
52 |
53 |
55 |
56 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/index.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * Entry TypeScript file for @alpa/app.
3 | * Created On 04 February 2022
4 | */
5 |
6 | import './index.css'
7 | import './nprogress.css'
8 | import 'tippy.js/dist/tippy.css'
9 | import 'tippy.js/animations/shift-away.css'
10 | import 'tippy.js/dist/border.css'
11 |
12 | import ReactDOM from 'react-dom'
13 |
14 | import { App } from './App'
15 |
16 | ReactDOM.render( , document.querySelector('#app'))
17 |
18 | const { registerSW } = await import('virtual:pwa-register')
19 | registerSW({
20 | immediate: true,
21 | })
22 |
--------------------------------------------------------------------------------
/app/src/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #EF233C;
8 | position: fixed;
9 | z-index: 1031;
10 | top: 0;
11 | left: 0;
12 | width: 100%;
13 | height: 2px;
14 | }
15 |
16 | /* Fancy blur effect */
17 | #nprogress .peg {
18 | display: block;
19 | position: absolute;
20 | right: 0px;
21 | width: 100px;
22 | height: 100%;
23 | box-shadow: 0 0 10px #EF233C, 0 0 5px #EF233C;
24 | opacity: 1.0;
25 |
26 | -webkit-transform: rotate(3deg) translate(0px, -4px);
27 | -ms-transform: rotate(3deg) translate(0px, -4px);
28 | transform: rotate(3deg) translate(0px, -4px);
29 | }
30 |
31 | /* Remove these to get rid of the spinner */
32 | #nprogress .spinner {
33 | display: block;
34 | position: fixed;
35 | z-index: 1031;
36 | top: 15px;
37 | right: 15px;
38 | }
39 |
40 | #nprogress .spinner-icon {
41 | width: 18px;
42 | height: 18px;
43 | box-sizing: border-box;
44 | border: solid 2px transparent;
45 | border-top-color: #EF233C;
46 | border-left-color: #EF233C;
47 | border-radius: 50%;
48 | -webkit-animation: nprogress-spinner 400ms linear infinite;
49 | animation: nprogress-spinner 400ms linear infinite;
50 | }
51 |
52 | .nprogress-custom-parent {
53 | overflow: hidden;
54 | position: relative;
55 | }
56 |
57 | .nprogress-custom-parent #nprogress .spinner,
58 | .nprogress-custom-parent #nprogress .bar {
59 | position: absolute;
60 | }
61 |
62 | @-webkit-keyframes nprogress-spinner {
63 | 0% {
64 | -webkit-transform: rotate(0deg);
65 | }
66 |
67 | 100% {
68 | -webkit-transform: rotate(360deg);
69 | }
70 | }
71 |
72 | @keyframes nprogress-spinner {
73 | 0% {
74 | transform: rotate(0deg);
75 | }
76 |
77 | 100% {
78 | transform: rotate(360deg);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/pages/Dash/Dash.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * A container component that will check if JWT is valid and redirect
3 | * to login if not, or else loads the Dashboard content.
4 | * Created On 08 February 2022
5 | */
6 |
7 | import { Dispatch, ReactElement, useEffect, useState } from 'react'
8 | import { useDispatch } from 'react-redux'
9 | import { useNavigate } from 'react-router-dom'
10 |
11 | import { CodeModalStateReturns } from '../../components/CodeModal'
12 | import getCodes from './codes'
13 | import { Content } from './Content'
14 |
15 | export const Dash = ({
16 | loading,
17 | quickText,
18 | setQuickText,
19 | setLoading,
20 | modalState,
21 | }: {
22 | loading: boolean
23 | quickText: string
24 | setQuickText: Dispatch>
25 | setLoading: React.Dispatch>
26 | modalState: CodeModalStateReturns
27 | }): ReactElement => {
28 | const navigate = useNavigate()
29 | const dispatch = useDispatch()
30 |
31 | // create any states, helper functions required for functioning
32 | const page = useState(0)
33 |
34 | useEffect(() => {
35 | getCodes({ navigate, dispatch, page, setLoading })
36 | }, [])
37 |
38 | return (
39 |
40 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/pages/Dash/codes.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Fetches the short codes accordingly to show on dash.
3 | * Created On 18 May 2022
4 | */
5 |
6 | import { Dispatch } from '@reduxjs/toolkit'
7 | import axios from 'axios'
8 | import progress from 'nprogress'
9 | import { NavigateFunction } from 'react-router-dom'
10 |
11 | import { login } from '../../store/auth'
12 | import { insert, setPages } from '../../store/codes'
13 | import logout from '../../util/logout'
14 | import scrolling from '../../util/scrolling'
15 | import { parseJWTPayload } from '../Login/index'
16 |
17 | const fetchCodes = async ({
18 | getCodesURL,
19 | apiToken,
20 | apiHost,
21 | dispatch,
22 | navigate,
23 | setLoading,
24 | }: {
25 | getCodesURL: () => string
26 | apiToken: string
27 | apiHost: string
28 | dispatch: Dispatch
29 | navigate: NavigateFunction
30 | setLoading: React.Dispatch>
31 | }): Promise => {
32 | try {
33 | // fetch the codes
34 | const { data } = await axios({
35 | method: 'GET',
36 | url: getCodesURL(),
37 | headers: {
38 | Authorization: `Bearer ${apiToken}`,
39 | },
40 | })
41 |
42 | // if (status != 200) throw new Error('Invalid response status')
43 | dispatch(setPages(data.pages))
44 | dispatch(insert(data.codes))
45 | setLoading(false)
46 | progress.done()
47 |
48 | return data.pages
49 | } catch {
50 | //
51 | logout({
52 | auth: {
53 | apiHost,
54 | apiToken,
55 | },
56 | dispatch,
57 | navigate,
58 | setLoading,
59 | })
60 |
61 | return -1
62 | }
63 | }
64 |
65 | export default async ({
66 | navigate,
67 | dispatch,
68 | page,
69 | setLoading,
70 | }: {
71 | dispatch: Dispatch
72 | navigate: NavigateFunction
73 | page: [number, any]
74 | setLoading: React.Dispatch>
75 | }) => {
76 | // start the progress bar
77 | progress.start()
78 |
79 | // fetch required variables from localStorage
80 | const apiToken = localStorage.getItem('apiToken') as string
81 | const apiHost = localStorage.getItem('apiHost') as string
82 |
83 | // handle when there's isn't an apiHost
84 | if (Boolean(apiHost) == false) {
85 | navigate('/login')
86 | progress.done()
87 | return
88 | }
89 |
90 | const getCodesURL = () => `${apiHost}/api/codes?page=${page[0]}`
91 |
92 | const pages = await fetchCodes({
93 | apiToken,
94 | getCodesURL,
95 | apiHost,
96 | dispatch,
97 | navigate,
98 | setLoading,
99 | })
100 |
101 | // set user's details into the store
102 | const { username, email } = parseJWTPayload(apiToken)
103 | dispatch(
104 | login({
105 | apiHost,
106 | apiToken,
107 | username,
108 | email,
109 | isLoggedIn: true,
110 | }),
111 | )
112 |
113 | // attach intersection observer
114 | scrolling({
115 | dispatch,
116 | apiHost,
117 | apiToken,
118 | pages,
119 | })
120 | }
121 |
--------------------------------------------------------------------------------
/app/src/pages/Login/Login.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * The login page which redirects to the dashboard
3 | * if the user is already logged in with a valid token.
4 | * Created On 08 February 2022
5 | */
6 |
7 | import { ReactElement, useEffect, useState } from 'react'
8 | import { useNavigate } from 'react-router-dom'
9 |
10 | import login, { openDashboard } from './index'
11 |
12 | export const Login = (): ReactElement => {
13 | const navigate = useNavigate()
14 |
15 | const [password, setPassword] = useState('')
16 | const [username, updateUsername] = useState(
17 | localStorage.getItem('username') || '',
18 | )
19 | const [apiHost, updateApiHost] = useState(
20 | localStorage.getItem('apiHost') || '',
21 | )
22 |
23 | const setUsername = (username: string) => {
24 | // set username in state
25 | updateUsername(username)
26 |
27 | // set username in localStorage
28 | localStorage.setItem('username', username)
29 | }
30 |
31 | const setApiHost = (host: string) => {
32 | // set API host in state
33 | updateApiHost(host)
34 |
35 | // set API host in localStorage
36 | localStorage.setItem('apiHost', host)
37 | }
38 |
39 | const submit = (e: any) => {
40 | // prevent page refresh
41 | e.preventDefault()
42 |
43 | // trigger the login function
44 | login({
45 | apiHost,
46 | navigate,
47 | credentials: {
48 | username,
49 | password,
50 | },
51 | })
52 |
53 | return false
54 | }
55 |
56 | // check if an existing token exists
57 | useEffect(() => {
58 | if (localStorage.getItem('apiToken')) openDashboard(navigate)
59 | }, [])
60 |
61 | return (
62 |
63 |
64 | {/* login card */}
65 |
66 | {/* card information */}
67 |
68 | Log in
69 |
70 |
71 | Welcome to{' '}
72 |
78 | alpa
79 |
80 | , please input the configured login credentials to
81 | manage your short links.
82 |
83 |
84 | {/* input fields */}
85 |
155 |
156 |
157 |
158 | )
159 | }
160 |
--------------------------------------------------------------------------------
/app/src/pages/Login/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Unified procedure to login a user with the given credentials.
3 | * Created On 12 February 2022
4 | */
5 |
6 | import axios from 'axios'
7 | import progress from 'nprogress'
8 | import { NavigateFunction } from 'react-router-dom'
9 |
10 | interface LoginOptions {
11 | navigate: NavigateFunction
12 | apiHost: string
13 | credentials: {
14 | username: string
15 | password: string
16 | }
17 | }
18 |
19 | export const openDashboard = (navigate: NavigateFunction) =>
20 | navigate('/', {
21 | replace: true,
22 | })
23 |
24 | export const parseJWTPayload = (token: string) => {
25 | const base64Url: string = token.split('.')[1]
26 | const base64: string = base64Url.replace(/-/g, '+').replace(/_/g, '/')
27 | const jsonPayload: any = decodeURIComponent(
28 | atob(base64)
29 | .split('')
30 | .map(c => {
31 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
32 | })
33 | .join(''),
34 | )
35 |
36 | return JSON.parse(jsonPayload)
37 | }
38 |
39 | export default async ({ apiHost, credentials, navigate }: LoginOptions) => {
40 | const { username, password } = credentials
41 |
42 | progress.start()
43 |
44 | try {
45 | const { status, data } = await axios({
46 | method: 'POST',
47 | url: `${apiHost}/api/auth/login`,
48 | data: {
49 | username,
50 | password,
51 | },
52 | })
53 |
54 | if (status == 200) {
55 | localStorage.setItem('apiToken', data.token)
56 | openDashboard(navigate)
57 | }
58 | } catch {
59 | console.log('failed login attempt')
60 | progress.done()
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/public/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsnthdev/alpa/e999519e5a6f2343ac596ac036ac549b9ae66ff5/app/src/public/cover.png
--------------------------------------------------------------------------------
/app/src/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsnthdev/alpa/e999519e5a6f2343ac596ac036ac549b9ae66ff5/app/src/public/icon.png
--------------------------------------------------------------------------------
/app/src/public/icon.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/app/src/public/siteicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/store/auth.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit'
2 |
3 | export interface AuthState {
4 | username: string
5 | email: string
6 | isLoggedIn: boolean
7 | apiHost: string
8 | apiToken: string
9 | }
10 |
11 | const initialState = {
12 | isLoggedIn: false,
13 | }
14 |
15 | const user = createSlice({
16 | initialState,
17 | name: 'user',
18 | reducers: {
19 | login: (state, action) => ({ ...state, ...action.payload }),
20 | logout: () => initialState,
21 | },
22 | })
23 |
24 | export const { login, logout } = user.actions
25 | export default user.reducer
26 |
--------------------------------------------------------------------------------
/app/src/store/codes.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit'
2 |
3 | interface CodeLink {
4 | url: string
5 | }
6 |
7 | export interface Code {
8 | code: string
9 | links: CodeLink[]
10 | tags: string
11 | }
12 |
13 | interface InitialState {
14 | pages: number
15 | codes: Code[]
16 | }
17 |
18 | const codes = createSlice({
19 | name: 'codes',
20 | initialState: {
21 | pages: 0,
22 | codes: [],
23 | } as InitialState,
24 | reducers: {
25 | // sets the total number of pages while
26 | // querying the API for infinite scrolling
27 | setPages: (state, action) => {
28 | const { codes } = state
29 |
30 | return {
31 | codes,
32 | pages: action.payload,
33 | }
34 | },
35 |
36 | // inserts the initial codes into the app store
37 | insert: (state, action) => {
38 | const { pages, codes } = state
39 |
40 | return {
41 | pages,
42 | codes: codes.concat(action.payload),
43 | }
44 | },
45 |
46 | // deletes a given short code given it's code string
47 | del: (state, action) => {
48 | const { pages, codes } = state
49 |
50 | return {
51 | pages,
52 | codes: codes.filter(
53 | (code: Code) => code.code != action.payload,
54 | ),
55 | }
56 | },
57 |
58 | // used to mutate an individual code object
59 | patch: (state, action) => {
60 | const { pages, codes } = state
61 |
62 | const index = codes.indexOf(
63 | codes.find(
64 | (code: Code) => code.code == action.payload.code,
65 | ) as any,
66 | )
67 |
68 | const newCodes = codes.filter(
69 | (code: Code) => code.code != action.payload.code,
70 | )
71 |
72 | newCodes.splice(index, 0, action.payload)
73 |
74 | return {
75 | pages,
76 | codes: newCodes,
77 | }
78 | },
79 |
80 | // used to update the entire codes array at once
81 | update: (state, { payload }) => {
82 | const { pages, codes } = state
83 |
84 | return {
85 | pages,
86 | codes: codes.concat(
87 | payload.filter(
88 | (code: Code) =>
89 | Boolean(
90 | codes.find((c: Code) => c.code == code.code),
91 | ) == false,
92 | ),
93 | ),
94 | }
95 | },
96 |
97 | // resets the state to initial and deletes all the
98 | // data from the frontend
99 | clear: () => {
100 | return {
101 | pages: 0,
102 | codes: [],
103 | }
104 | },
105 | },
106 | })
107 |
108 | export const { insert, clear, del, patch, update, setPages } = codes.actions
109 | export default codes.reducer
110 |
--------------------------------------------------------------------------------
/app/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit'
2 |
3 | import auth, { AuthState } from './auth.js'
4 | import codes, { Code } from './codes.js'
5 |
6 | export interface AppState {
7 | auth: AuthState
8 | codes: {
9 | pages: number
10 | codes: Code[]
11 | }
12 | }
13 |
14 | export const store = configureStore({
15 | reducer: { auth, codes },
16 | })
17 |
--------------------------------------------------------------------------------
/app/src/util/hotkeys.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Contains hotkey map and their handlers.
3 | * Created On 14 May 2022
4 | */
5 |
6 | export const hotkeyMap = {
7 | SEARCH: '/',
8 | NEWCODE: 'n',
9 | }
10 |
11 | export const hotkeyHandlers = {
12 | SEARCH: (e: any) => {
13 | // eslint-disable-next-line prettier/prettier
14 | (document.querySelector('#txtSearch') as any).focus()
15 | e.preventDefault()
16 | return false
17 | },
18 | NEWCODE: (e: any) => {
19 | // eslint-disable-next-line prettier/prettier
20 | (document.querySelector('#btnNew') as any).click()
21 | e.preventDefault()
22 | return false
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/util/logout.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Unified procedure to logout the authenticated user.
3 | * Created On 12 February 2022
4 | */
5 |
6 | import { Dispatch } from '@reduxjs/toolkit'
7 | import axios from 'axios'
8 | import progress from 'nprogress'
9 | import { NavigateFunction } from 'react-router-dom'
10 |
11 | import { logout } from '../store/auth'
12 | import { clear } from '../store/codes'
13 |
14 | interface LogoutOptions {
15 | auth: {
16 | apiHost: string
17 | apiToken: string
18 | }
19 | navigate: NavigateFunction
20 | dispatch: Dispatch
21 | setLoading: React.Dispatch>
22 | }
23 |
24 | export default ({ auth, navigate, dispatch, setLoading }: LogoutOptions) => {
25 | const { apiHost, apiToken } = auth
26 |
27 | // the logout procedure
28 | const procedure = () => {
29 | // delete the token from the browser
30 | localStorage.removeItem('apiToken')
31 |
32 | // reset our app store
33 | dispatch(logout())
34 | dispatch(clear())
35 |
36 | // go back to login page
37 | navigate('/login')
38 | progress.done()
39 |
40 | // set the loading state back to true
41 | setLoading(true)
42 | }
43 |
44 | progress.start()
45 | axios({
46 | method: 'DELETE',
47 | url: `${apiHost}/api/auth/logout`,
48 | headers: {
49 | Authorization: `Bearer ${apiToken}`,
50 | },
51 | })
52 | .then(() => procedure())
53 | .catch(e => {
54 | // if the token is no longer authorized, we simply
55 | // clean up the token and redirect to login page
56 | if (JSON.parse(JSON.stringify(e)).status == 401) procedure()
57 | if (JSON.parse(JSON.stringify(e)).status == 500) procedure()
58 | })
59 | .finally(() => {
60 | progress.done()
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/util/scrolling.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from '@reduxjs/toolkit'
2 | import axios from 'axios'
3 |
4 | import { insert } from '../store/codes'
5 |
6 | export default async ({
7 | apiHost,
8 | apiToken,
9 | pages,
10 | dispatch,
11 | }: {
12 | pages: number
13 | apiHost: string
14 | apiToken: string
15 | dispatch: Dispatch
16 | }) => {
17 | let currentPage = 0
18 | let loading = false
19 |
20 | const lastOne = document.querySelector(
21 | '#codes > div:last-child',
22 | ) as HTMLDivElement
23 |
24 | const observer = new IntersectionObserver(async entries => {
25 | const entry = entries[0]
26 |
27 | if (
28 | entry.intersectionRatio > 0 &&
29 | loading == false &&
30 | currentPage < pages
31 | ) {
32 | loading = true
33 | currentPage = currentPage + 1
34 |
35 | const { data } = await axios({
36 | method: 'GET',
37 | url: `${apiHost}/api/codes?page=${currentPage}`,
38 | headers: {
39 | Authorization: `Bearer ${apiToken}`,
40 | },
41 | })
42 |
43 | if (data.codes.length != 0) {
44 | dispatch(insert(data.codes))
45 |
46 | const newLastOne = document.querySelector(
47 | '#codes > div:last-child',
48 | ) as HTMLDivElement
49 |
50 | observer.observe(newLastOne)
51 | }
52 |
53 | observer.unobserve(lastOne)
54 | loading = false
55 | }
56 | })
57 |
58 | try {
59 | observer.observe(lastOne)
60 | } catch {
61 | return
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/util/searchAPI.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Searches the api provided authentication details.
3 | * Created On 14 May 2022
4 | */
5 |
6 | import axios from 'axios'
7 | import progress from 'nprogress'
8 |
9 | import { AuthState } from '../store/auth'
10 | import { update } from '../store/codes'
11 |
12 | // fetch new codes upon searching
13 | export const searchAPI = ({
14 | auth,
15 | dispatch,
16 | quickText,
17 | }: {
18 | auth: AuthState
19 | quickText: string
20 | dispatch: any
21 | }): Promise =>
22 | new Promise((resolve, reject) => {
23 | progress.start()
24 | axios({
25 | method: 'GET',
26 | url: `${auth.apiHost}/api/codes?search=${encodeURIComponent(
27 | quickText,
28 | )}`,
29 | headers: {
30 | Authorization: `Bearer ${auth.apiToken}`,
31 | },
32 | })
33 | .then(({ data }) => {
34 | dispatch(update(data.codes))
35 | resolve(true)
36 | })
37 | .catch(err => reject(err))
38 | .finally(() => progress.done())
39 | })
40 |
--------------------------------------------------------------------------------
/app/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /*
2 | * TailwindCSS configuration for @alpa/app project.
3 | * Created On 08 February 2022
4 | */
5 |
6 | module.exports = {
7 | darkMode: 'class',
8 | content: ['./src/**/*.{js,jsx,ts,tsx,html}'],
9 | theme: {
10 | fontFamily: {
11 | sans: [
12 | 'Plus Jakarta Sans',
13 | 'ui-sans-serif',
14 | 'system-ui',
15 | '-apple-system',
16 | 'BlinkMacSystemFont',
17 | '"Segoe UI"',
18 | 'Roboto',
19 | '"Helvetica Neue"',
20 | 'Arial',
21 | '"Noto Sans"',
22 | 'sans-serif',
23 | ],
24 | mono: [
25 | 'IBM Plex Mono',
26 | 'ui-monospace',
27 | 'SFMono-Regular',
28 | 'Menlo',
29 | 'Monaco',
30 | 'Consolas',
31 | '"Liberation Mono"',
32 | '"Courier New"',
33 | 'monospace',
34 | ],
35 | },
36 | extend: {
37 | colors: {
38 | primary: '#EF233C',
39 | 'primary-hover': '#D11026',
40 | secondary: '#1C1917',
41 | 'secondary-hover': '#5A5049',
42 | },
43 | },
44 | },
45 | }
46 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "strict": true,
5 | "outDir": "dist",
6 | "jsx": "preserve",
7 | "target": "ESNext",
8 | "module": "ESNext",
9 | "esModuleInterop": true,
10 | "moduleResolution": "node",
11 | "allowSyntheticDefaultImports": true,
12 | "types": [
13 | "vite/client"
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/vite.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Vite bundler configuration for @alpa/app project.
3 | * Created On 04 February 2022
4 | */
5 |
6 | import react from '@vitejs/plugin-react'
7 | import autoprefixer from 'autoprefixer'
8 | import tailwindcss from 'tailwindcss'
9 | import { defineConfig } from 'vite'
10 | import { createHtmlPlugin } from 'vite-plugin-html'
11 | import { VitePWA } from 'vite-plugin-pwa'
12 |
13 | export default defineConfig(() => {
14 | return {
15 | clearScreen: false,
16 | root: 'src',
17 | build: {
18 | outDir: '../dist',
19 | minify: 'esbuild',
20 | target: 'esnext',
21 | polyfillModulePreload: false,
22 | },
23 | server: {
24 | fs: {
25 | strict: false,
26 | },
27 | },
28 | css: {
29 | postcss: {
30 | plugins: [autoprefixer(), tailwindcss()],
31 | },
32 | },
33 | plugins: [
34 | react(),
35 | createHtmlPlugin({
36 | minify: true,
37 | }),
38 | VitePWA({
39 | manifest: {
40 | name: 'alpa',
41 | short_name: 'alpa',
42 | id: 'dev.vsnth.alpa',
43 | description: 'A fast ⚡ self-hosted link 🔗 shortener.',
44 | orientation: 'portrait-primary',
45 | theme_color: '#FFFFFF',
46 | start_url: '/app',
47 | icons: [
48 | {
49 | src: 'https://alpa.link/app/icon.svg',
50 | sizes: '791x791',
51 | type: 'image/svg',
52 | purpose: 'maskable',
53 | },
54 | {
55 | src: 'https://alpa.link/app/icon.png',
56 | sizes: '791x791',
57 | type: 'image/png',
58 | },
59 | ],
60 | },
61 | includeAssets: ['/cover.png', '/.well-known/assetlinks.json'],
62 | }),
63 | ],
64 | }
65 | })
66 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | # the redis Docker image with RedisJSON and RediSearch
4 | # modules enabled along with persistance
5 | redis:
6 | image: redislabs/redisearch:2.4.0
7 | container_name: redis
8 | command: redis-server --loadmodule /usr/lib/redis/modules/redisearch.so --loadmodule /usr/lib/redis/modules/rejson.so --appendonly yes
9 | volumes:
10 | - .redis:/data
11 |
12 | # the API backend which is the actual
13 | # redirection service
14 | alpa-api:
15 | image: vsnthdev/alpa-api
16 | container_name: alpa-api
17 | ports:
18 | - 1727:1727
19 | volumes:
20 | - ./api/config.yml:/opt/alpa/api/config.yml
21 |
22 | # the frontend app to interface with the API
23 | alpa-app:
24 | image: vsnthdev/alpa-app
25 | container_name: alpa-app
26 | ports:
27 | - 3000:3000
28 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ( अल्प ) — A fast ⚡ self-hosted link 🔗 shortener.
14 |
15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Reads the TypeScript code in this repository and programmatically generates documentation markdown files.
34 |
35 | ## 🔮 Tech stack
36 |
37 | | Name | Description |
38 | | --- | --- |
39 | |
**Handlebars** | Templating engine to inject values into template markdown files. |
40 | |
**Chokidar** | Watches for file changes and rebuilds docs. |
41 |
42 | ## 💻 Building & Dev Setup
43 |
44 | You need to be at least on **Node.js v17.4.0 or above** and follow the below instructions to build this project 👇
45 |
46 | - **STEP 1️⃣** Clone this repository & enter into it (`cd ./alpa`)
47 | - **STEP 2️⃣** Run **`npm install`** to get all dependencies & link projects together
48 | - **STEP 3️⃣** Enter in the project directory (`cd docs`)
49 | - **STEP 4️⃣** To build this project run **`npm run build`**
50 |
51 | Upon building `@alpa/docs` will rerender all markdown files within all the projects in this repository.
52 |
53 | > **ℹ️ Info:** You can also run `npm run clean` to delete existing documentation from the project to avoid overwriting & purge dangling documents. While running the `build` script, old docs are first deleted before getting overwritten.
54 |
55 | ## 📰 License
56 | > The **alpa** project is released under the [AGPL-3.0-only](https://github.com/vsnthdev/alpa/blob/main/LICENSE.md).
Developed & maintained By Vasanth Srivatsa. Copyright 2022 © Vasanth Developer.
57 |
58 |
59 | > vsnth.dev ·
60 | > YouTube @vasanthdeveloper ·
61 | > Twitter @vsnthdev ·
62 | > Discord Vasanth Developer
63 |
--------------------------------------------------------------------------------
/docs/md/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{#if isIndex}}
{{/if}}
12 |
13 | {{desc}}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | **alpa** is a self-hosted _(you run it on your servers)_ URL shortener which is fast and provides full control of the short links you create.
36 |
37 | It takes this 👇
38 |
39 | ```plaintext
40 | https://vasanthdeveloper.com/migrating-from-vps-to-kubernetes
41 | ```
42 |
43 | and converts it into something like this 👇
44 |
45 | ```plaintext
46 | https://vas.cx/fjby
47 | ```
48 |
49 | Which is easier to remember and share across the internet.
50 |
51 | ## ✨ Features
52 |
53 | - **It is 🚀 super fast**
54 | - **Your domain, your branding** 👌
55 | - **Privacy friendly 🤗 & configurable**
56 | - **Simple & 🎮 intuitive dashboard**
57 |
58 | ## 💡 Why I built it?
59 |
60 | I was using goo.gl back in 2016 and I was very impressed by it. It's simple dashboard & fast redirection were two things that were really attractive to me. **alpa** is inspired by goo.gl URL shortener.
61 |
62 | Along with that, most popular URL shorteners are not _self-hosted_, which means that you'll share your data with others that use the service. To me, it was a concern about **reliability**, **privacy** and **performance**.
63 |
64 | ## 🚀 Quick start
65 |
66 | The quickest way to run **alpa** is through Docker Compose using only **3 steps**:
67 |
68 | **STEP 1️⃣** Getting alpa
69 |
70 | Once you have Docker Compose installed, clone this repository by running the following command 👇
71 |
72 | ```
73 | git clone https://github.com/vsnthdev/alpa.git
74 | ```
75 |
76 | **STEP 2️⃣** Creating a configuration file
77 |
78 | Enter into the **alpa** directory and create an API config by running 👇
79 |
80 | ```
81 | cd ./alpa
82 | cp ./api/config.example.yml ./api/config.yml
83 | ```
84 |
85 | **⚠️ Warning:** The example config file is only meant for development and testing purposes, a proper config file is required to securely run **alpa** in production.
86 |
87 | **STEP 3️⃣** Starting alpa
88 |
89 | Now all you need to do is, run the following command to start both **alpa**'s [app](https://github.com/vsnthdev/alpa/tree/main/app) & the [API](https://github.com/vsnthdev/alpa/tree/main/api).
90 |
91 | ```
92 | docker-compose up -d
93 | ```
94 |
95 | ## ⚡ Support & funding
96 |
97 | Financial funding would really help this project go forward as I will be able to spend more hours working on the project to maintain & add more features into it.
98 |
99 | Please get in touch with me on [Discord](https://discord.com/users/492205153198407682) or [Twitter](https://vas.cx/twitter) to get fund the project even if it is a small amount 🙏
100 |
101 | ## 🤝 Troubleshooting & help
102 |
103 | If you face trouble setting up **alpa**, or have any questions, or even a bug report, feel free to contact me through Discord. I provide support for **alpa** on [my Discord server](https://vas.cx/discord).
104 |
105 | I will be happy to consult & personally assist you 😊
106 |
107 | ## 💖 Code & contribution
108 |
109 | **Pull requests are always welcome** 👏
110 |
111 | But it will be better if you can get in touch with me before contributing or [raise an issue](https://github.com/vsnthdev/alpa/issues/new/choose) to see if the contribution aligns with the vision of the project.
112 |
113 | > **ℹ️ Note:** This project follows [Vasanth's Commit Style](https://vas.cx/commits) for commit messages. We highly encourage you to use this commit style for contributions to this project.
114 |
115 | ## 💻 Building & Dev Setup
116 |
117 | This is a [monorepo](https://monorepo.tools/#what-is-a-monorepo) containing multiple projects. Below is a list of all the projects in this repository, what they do, and docs to building them 👇
118 |
119 | | Name | Description |
120 | | --- | --- |
121 | {{#each projects}}
122 | | [{{this.name}}](./{{this.projectName}}) | {{this.description}} |
123 | {{/each}}
124 |
125 | ### 🛠️ Building all projects
126 |
127 | You need to be at least on **Node.js v{{nodeVersion}} or above** and follow the below instructions to build all the projects 👇
128 |
129 | - **STEP 1️⃣** Clone this repository & enter into it (`cd ./alpa`)
130 | - **STEP 2️⃣** Run **`npm install`** to get all dependencies & link projects together
131 | - **STEP 3️⃣** To build all the projects & docs run **`npm run build`**
132 |
133 | ### 🐳 Building Docker images
134 |
135 | Instead of pulling Docker images from DockerHub, you can build yourself by running 👇
136 |
137 | ```
138 | npm run build:docker
139 | ```
140 |
141 | > ⚠️ **Warning:** Make sure to delete Docker images pulled from DockerHub or a previous build, to prevent conflicts before running the above command.
142 |
143 | ### 🍃 Cleaning project
144 |
145 | Building the project generates artifacts on several places in the project. To delete all those artifacts **(including docs)**, run the below command 👇
146 |
147 | ```
148 | npm run clean
149 | ```
150 |
151 |
152 |
153 | ## 📰 License
154 | > The **alpa** project is released under the [{{license}}](https://github.com/vsnthdev/alpa/blob/main/LICENSE.md).
Developed & maintained By Vasanth Srivatsa. Copyright {{year}} © Vasanth Developer.
155 |
156 |
157 | > vsnth.dev ·
158 | > YouTube @vasanthdeveloper ·
159 | > Twitter @vsnthdev ·
160 | > Discord Vasanth Developer
161 |
--------------------------------------------------------------------------------
/docs/md/api/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: ../README.md
3 | ---
4 |
5 | This is the core of the project, it the RESTful API that performs redirection, communicates with the database and provides **alpa** it's functionality.
6 |
7 | ## ⚙️ Configuration
8 |
9 | Refer to the [config example file](https://github.com/vsnthdev/alpa/blob/main/api/config.example.yml) for all possible configuration keys, and their detailed explanation. If you still have any doubts, feel free to shoot a tweet at me [@vsnthdev](https://vas.cx/@me).
10 |
11 | ## 🔭 API Routes
12 |
13 | | Method | Path | Description | Protected |
14 | |---|---|---|---|
15 | {{#each api.routes}}
16 | | `{{this.method}}` | `{{this.path}}` | {{this.description}} | {{#if this.authRequired}}✅{{else}}❌{{/if}} |
17 | {{/each}}
18 |
19 | ## 🔮 Tech stack
20 |
21 | | Name | Description |
22 | | --- | --- |
23 | |
**Fastify** | HTTP server focussed on speed designed to build RESTful APIs. |
24 | |
**JSON Web Tokens** | For user authentication. |
25 | |
**Redis** | Key-value pair database known for it's speed. |
26 | |
**RedisJSON** | Redis database plugin to store JSON documents. |
27 | |
**RediSearch** | Redis database plugin that facilitates full text search. |
28 | |
**Docker** | For easy installation & seamless updates. |
29 | |
**Kubernetes** | For scalable deployments to production. |
30 |
31 | ## 💻 Building & Dev Setup
32 |
33 | You need to be at least on **Node.js v{{nodeVersion}} or above** and follow the below instructions to build this project 👇
34 |
35 | - **STEP 1️⃣** Clone this repository & enter into it (`cd ./alpa`)
36 | - **STEP 2️⃣** Run **`npm install`** to get all dependencies & link projects together
37 | - **STEP 3️⃣** Enter in the project directory (`cd api`)
38 | - **STEP 4️⃣** To build this project run **`npm run build`**
39 |
40 | Upon building `@alpa/api` a `dist` folder is created with the transpiled JavaScript files.
41 |
--------------------------------------------------------------------------------
/docs/md/api/docs/docker.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: ../../README.md
3 | ---
4 |
5 | # 🐳 Deploying with Docker Compose
6 |
7 | There are mainly 3 ways to deploy `@alpa/api` onto production. For personal usage deploying through 🐳 **Docker Compose** is the easiest & recommended way. For advanced use cases or high intensity workload read about [manual deployment](./manual.md) & [Kubernetes deployment](./kubernetes.md).
8 |
9 | Deploying **alpa**'s API using Docker is easy and straightforward by following the below steps:
10 |
11 | ## 🔍 Prerequisites
12 |
13 | 1. [Docker v20.10.13](https://docs.docker.com/engine/install) or higher
14 | 2. [Docker Compose v2.3.2](https://docs.docker.com/compose/cli-command) or [`docker-compose` v1.29.2](https://docs.docker.com/compose/install)
15 |
16 | ## 🚀 Deployment process
17 |
18 | Once you've satisfied the prerequisites, follow the below steps to configure `@alpa/api` to run in production.
19 |
20 | ### 📂 Create a new folder
21 |
22 | Create a new folder named `alpa` and enter into it, this is where we'll store the `docker-compose.yml` and other artifacts generated for running all the services we need, by running 👇 the following command:
23 |
24 | ```
25 | mkdir ./alpa && cd ./alpa
26 | ```
27 |
28 | ### 📃 Creating `docker-compose.yml` file
29 |
30 | The first thing we'll do in the newly created folder is create a `docker-compose.yml` file which defines all the services that are required for `@alpa/api`.
31 |
32 | Open your favourite text editor (preferably [VSCode](https://code.visualstudio.com)), copy the below code block 👇 and save it as `docker-compose.yml` in the `alpa` directory.
33 |
34 | ```yaml
35 | version: "3.8"
36 | services:
37 | # the redis Docker image with RedisJSON and RediSearch
38 | # modules pre-configured & enabled along with persistance
39 | redis:
40 | image: redislabs/redisearch:2.4.0
41 | container_name: redis
42 | command: redis-server --loadmodule /usr/lib/redis/modules/redisearch.so --loadmodule /usr/lib/redis/modules/rejson.so --appendonly yes
43 | volumes:
44 | - .redis:/data
45 |
46 | # @alpa/api Docker image
47 | alpa-api:
48 | image: vsnthdev/alpa-api:v{{api.app.version}}
49 | container_name: alpa-api
50 | ports:
51 | - 1727:1727
52 | volumes:
53 | - ./config.yml:/opt/alpa/api/config.yml
54 | ```
55 |
56 | > ℹ️ **Info:** We're intensionally using a versioned images, to mitigate the risk of accidentally updating the image and breaking everything.
57 |
58 | ### ⚙️ Mounting configuration file
59 |
60 | An example config file will all possible values already exists in this repository. Simply right click on [this link](https://raw.githubusercontent.com/vsnthdev/alpa/main/api/config.example.yml) and select "_Save as_".
61 |
62 | Now save it with the name `config.yml` in the `alpa` folder where `docker-compose.yml` is. Once done your `alpa` folder should contain two files
63 |
64 | ```
65 | docker-compose.yml
66 | config.yml
67 | ```
68 |
69 | ### ⚡ Configuring for production
70 |
71 | Provided example config file is best suitable for development & testing purposes only. We need to make some changes to the config file to make `@alpa/api` suitable for production environments.
72 |
73 | These exact changes have been specified in the manual deployment docs **[click here to view them](./manual.md#-production-configuration).**
74 |
75 | > ⚠️ **Warning:** Do not use `@alpa/api` in production without following the production configuration steps. It will lead to serious security risks and instabilities.
76 |
77 | ### ✨ Starting `@alpa/api`
78 |
79 | With the above mentioned changes being done to the configuration file, `@alpa/api` is now ready to be started in a production environment safely.
80 |
81 | To start all the services defined in our `docker-compose.yml` run 👇 one of the below commands depending on your Docker Compose version:
82 |
83 | ```bash
84 | # if you're on Docker Compose v2
85 | docker compose up
86 |
87 | # if you're on docker-compose v1
88 | docker-compose up
89 | ```
90 |
91 | After following the above steps you should be able to login from the configured client and start enjoying **alpa**.
92 |
93 | **If you're still facing issues, refer the [troubleshooting & help section](https://github.com/vsnthdev/alpa#-troubleshooting--help) for further information.**
94 |
95 |
96 |
--------------------------------------------------------------------------------
/docs/md/api/docs/kubernetes.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: ../../README.md
3 | ---
4 |
5 | # 🏅 Deploying with Kubernetes
6 |
7 | There are mainly 3 ways to deploy `@alpa/api` onto production. For personal usage deploying through [🐳 Docker Compose](./docker.md) is the most easiest & recommended way. For advanced use cases read about [manual deployment](./manual.md).
8 |
9 | Deploying **alpa**'s API into a Kubernetes Cluster is easy and straightforward by following the below steps:
10 |
11 | ## 🔍 Prerequisites
12 |
13 | 1. [Docker v20.10.13](https://docs.docker.com/engine/install) or higher
14 | 2. [Kubernetes 1.22.5](https://kubernetes.io/docs/setup) or higher
15 |
16 | ## 🚀 Deployment process
17 |
18 | Once you've satisfied the prerequisites, follow the below steps to configure `@alpa/api` to run in production.
19 |
20 | ### 📂 Creating folder structure
21 |
22 | Create a new folder named `alpa` with two sub-folders named `alpa`, `redis`, this is where we'll store the Kubernetes files.
23 |
24 | ```
25 | mkdir alpa && mkdir alpa/redis && mkdir alpa/alpa && cd alpa
26 | ```
27 |
28 | ### 🏝️ Creating a namespace
29 |
30 | Using your favorite text editor, create a new file named `0-namespace.yml` file and paste the below contents 👇
31 |
32 | ```yml
33 | apiVersion: v1
34 | kind: Namespace
35 | metadata:
36 | name: alpa
37 | ```
38 |
39 | Save the `0-namespace.yml` file in the `alpa` folder we created above.
40 |
41 | ### 🪛 Setting up Redis database
42 |
43 | To setup Redis database in a Kubernetes cluster, we need to create a few files. Lets create them one by one while going through each one.
44 |
45 | #### 🧳 Redis database volume
46 |
47 | Create a file named `1-volumes.yml` and save the below contents 👇 in the `alpa/redis` folder we created.
48 |
49 | ```yml
50 | apiVersion: v1
51 | kind: PersistentVolumeClaim
52 | metadata:
53 | name: redis-claim
54 | namespace: alpa
55 | spec:
56 | resources:
57 | requests:
58 | storage: 1G
59 | volumeMode: Filesystem
60 | accessModes:
61 | - ReadWriteOnce
62 | ```
63 |
64 | This file creates a [claim for a persistent volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes) which can later be used to create an actual volume to store data from our Redis database.
65 |
66 | #### 📌 Redis database deployment
67 |
68 | Create a file named `2-deploys.yml` and save the below contents 👇 in the `alpa/redis` folder we created.
69 |
70 | ```yml
71 | apiVersion: apps/v1
72 | kind: Deployment
73 | metadata:
74 | name: redis
75 | namespace: alpa
76 | spec:
77 | selector:
78 | matchLabels:
79 | app: redis
80 | template:
81 | metadata:
82 | labels:
83 | app: redis
84 | spec:
85 | hostname: redis
86 | volumes:
87 | - name: redis
88 | persistentVolumeClaim:
89 | claimName: redis-claim
90 | containers:
91 | - name: redis
92 | image: redislabs/redisearch:2.4.0
93 | imagePullPolicy: IfNotPresent
94 | args:
95 | - "redis-server"
96 | - "--loadmodule"
97 | - "/usr/lib/redis/modules/redisearch.so"
98 | - "--loadmodule"
99 | - "/usr/lib/redis/modules/rejson.so"
100 | - "--appendonly"
101 | - "yes"
102 | volumeMounts:
103 | - mountPath: /data
104 | name: redis
105 | resources:
106 | limits:
107 | memory: 128Mi
108 | cpu: 100m
109 | ports:
110 | - containerPort: 6379
111 | ```
112 |
113 | This is the actual deployment file that tells Kubernetes which Docker container to run and how to link it with our Persistent Volume Claim and mount the data directory.
114 |
115 | This file also specifies how much CPU & memory is allocated to the Redis database.
116 |
117 | #### 🔦 Redis database service
118 |
119 | Create a file named `3-services.yml` and save the below contents 👇 in the `alpa/redis` folder we created.
120 |
121 | ```yml
122 | apiVersion: v1
123 | kind: Service
124 | metadata:
125 | name: redis
126 | namespace: alpa
127 | spec:
128 | type: NodePort
129 | selector:
130 | app: redis
131 | ports:
132 | - port: 6379
133 | targetPort: 6379
134 | ```
135 |
136 | Redis service exposes the Redis database on port 6379 to be accessed by `@alpa/api` and other deployments in this namespace.
137 |
138 | > ℹ️ **Note:** For security purposes, it is recommended that you change this port number `6379` to a random 5 digit number below 60,000.
139 |
140 | ### ⚙️ Creating configuration file
141 |
142 | Create a file named `1-configs.yml` and save the below contents 👇 in the `alpa/alpa` folder we created.
143 |
144 | ```yml
145 | apiVersion: v1
146 | kind: ConfigMap
147 | metadata:
148 | name: alpa-api-config
149 | namespace: alpa
150 | data:
151 | config: |
152 | {{{yaml api.config 8}}}
153 | ```
154 |
155 | This creates a [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap) in Kubernetes which stores the config file for `@alpa/api` which will be mounted as a volume later.
156 |
157 | ### ⚡ Configuring for production
158 |
159 | Provided example config file is best suitable for development & testing purposes only. We need to make some changes to the config file to make `@alpa/api` suitable for production environments.
160 |
161 | These exact changes have been specified in the manual deployment docs **[click here to view them](./manual.md#-production-configuration).**
162 |
163 | > ⚠️ **Warning:** Do not use `@alpa/api` in production without following the production configuration steps. It will lead to serious security risks and instabilities.
164 |
165 | #### 📌 Deploying `@alpa/api`
166 |
167 | Create a file named `2-deploys.yml` and save the below contents 👇 in the `alpa/alpa` folder we created.
168 |
169 | ```yml
170 | apiVersion: apps/v1
171 | kind: Deployment
172 | metadata:
173 | name: alpa
174 | namespace: alpa
175 | spec:
176 | selector:
177 | matchLabels:
178 | app: alpa
179 | template:
180 | metadata:
181 | labels:
182 | app: alpa
183 | spec:
184 | hostname: alpa
185 | volumes:
186 | - name: alpa-api-config
187 | configMap:
188 | name: alpa-api-config
189 | containers:
190 | - name: alpa
191 | image: vsnthdev/alpa-api:v{{api.app.version}}
192 | imagePullPolicy: Always
193 | volumeMounts:
194 | - mountPath: /opt/alpa/api/config.yml
195 | name: alpa-api-config
196 | subPath: config
197 | readOnly: true
198 | resources:
199 | limits:
200 | memory: 256Mi
201 | cpu: 100m
202 | ports:
203 | - containerPort: 1727
204 | ```
205 |
206 | > ℹ️ **Info:** We're intensionally using a versioned images, to mitigate the risk of accidentally updating the image and breaking everything.
207 |
208 | This file tells Kubernetes to pull and run `@alpa/api` on Kubernetes along with how much memory and CPU should be allocated.
209 |
210 | ### 🌏 Creating `@alpa/api` service
211 |
212 | Create a file named `3-services.yml` and save the below contents 👇 in the `alpa/alpa` folder we created.
213 |
214 | ```yml
215 | apiVersion: v1
216 | kind: Service
217 | metadata:
218 | name: alpa
219 | namespace: alpa
220 | spec:
221 | type: NodePort
222 | selector:
223 | app: alpa
224 | ports:
225 | - port: 48878
226 | targetPort: 1727
227 | ```
228 |
229 | A [service](https://kubernetes.io/docs/concepts/services-networking/service) will allow you to access `@alpa/api` outside the Kubernetes cluster network on port `48878`.
230 |
231 | > ℹ️ **Note:** For security purposes, it is recommended that you change this port number `48878` to a random 5 digit number below 60,000.
232 |
233 | ### 🔨 Creating `kustomization.yml` file
234 |
235 | Create a file named `kustomization.yml` and save the below contents 👇 in the `alpa` folder we created.
236 |
237 | ```yml
238 | apiVersion: kustomize.config.k8s.io/v1beta1
239 | resources:
240 | # deleting the name will delete everything
241 | - 0-namespace.yml
242 |
243 | # redis database for primarily for alpa
244 | - redis/1-volumes.yml
245 | - redis/2-deploys.yml
246 | - redis/3-services.yml
247 |
248 | # @alpa/api service
249 | - alpa/1-configs.yml
250 | - alpa/2-deploys.yml
251 | - alpa/3-services.yml
252 | ```
253 |
254 | Once all the required files are created, the completed directory structure should look something like 👇
255 |
256 | ```js
257 | alpa
258 | /alpa
259 | 1-configs.yml
260 | 2-deploys.yml
261 | 3-services.yml
262 | /redis
263 | 1-volumes.yml
264 | 2-deploys.yml
265 | 3-services.yml
266 | 0-namespace.yml
267 | kustomization.yml
268 | ```
269 |
270 | ### ✨ Starting `@alpa/api`
271 |
272 | With the above mentioned changes being done to the configuration file, `@alpa/api` is now ready to be started in a production environment safely.
273 |
274 | To start all the services defined in our `kustomization.yml` run 👇 the below command:
275 |
276 | ```bash
277 | kubectl apply -k .
278 | ```
279 |
280 | **If you're still facing issues, refer the [troubleshooting & help section](https://github.com/vsnthdev/alpa#-troubleshooting--help) for further information.**
281 |
--------------------------------------------------------------------------------
/docs/md/api/docs/manual.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: ../../README.md
3 | ---
4 |
5 | # 🧰 Manually deploying
6 |
7 | There are mainly 3 ways to deploy `@alpa/api` onto production. For personal usage deploying through [🐳 Docker Compose](./docker.md) is the most easiest & recommended way. For high intensity workloads read about [Kubernetes deployment](./kubernetes.md).
8 |
9 | Deploying **alpa**'s API is easy and straightforward by following the below steps:
10 |
11 | Manually deploying will allow you to run `@alpa/api` on a production server without additional layers of abstraction.
12 |
13 | > **⚠️ Warning:** This method is good for advanced use cases & updating alpa may not be straightforward always.
14 |
15 | ## 🔍 Prerequisites
16 |
17 | 1. Node.js version v{{nodeVersion}} or higher ([Windows](https://youtu.be/sHGz607fsVA) / [Linux](https://github.com/nodesource/distributions#readme) / [macOS](https://github.com/nvm-sh/nvm#readme))
18 | 2. [Redis database](https://redis.io)
19 | 3. [RedisJSON plugin](https://redis.io/docs/stack/json/) for Redis database
20 | 4. [RediSearch plugin](https://redis.io/docs/stack/search) for Redis database
21 |
22 | ## 🚀 Deployment process
23 |
24 | As said in this [README.md](https://github.com/vsnthdev/alpa/tree/main#readme) file **alpa** is a monorepo containing multiple projects, follow the below steps to configure `@alpa/api` to run in production.
25 |
26 | ### 💾 Getting `@alpa/api`
27 |
28 | Instead of normally cloning entire repository here are the commands to only clone `@alpa/api` project & the root project 👇
29 |
30 | **STEP 1️⃣** Clone only the root project
31 |
32 | ```
33 | git clone --single-branch --branch main --depth 1 --filter=blob:none --sparse https://github.com/vsnthdev/alpa
34 | ```
35 |
36 | **STEP 2️⃣** Enter the freshly cloned root project
37 |
38 | ```
39 | cd ./alpa
40 | ```
41 |
42 | **STEP 3️⃣** Initialize Git sparse checkout
43 |
44 | ```
45 | git sparse-checkout init --cone
46 | ```
47 |
48 | **STEP 4️⃣** Pull only `@alpa/api` project while ignoring other projects
49 |
50 | ```
51 | git sparse-checkout set api
52 | ```
53 |
54 | ### 🪄 Installing dependencies
55 |
56 | Dependency libraries for both the root project & `@alpa/api` can be installed & setup by running the following command 👇
57 |
58 | ```
59 | npm install
60 | ```
61 |
62 | ### 💻 Building `@alpa/api`
63 |
64 | We only store TypeScript source code in this repository so before we can start `@alpa/api` server, we need to build (_transpile TypeScript into JavaScript_) the project using the following command 👇
65 |
66 | ```
67 | npm run build
68 | ```
69 |
70 | ### ⚙️ Creating configuration file
71 |
72 | An [example config file](../../api/config.example.yml) is already present with all configurable values and their defaults. We'll copy that and make some necessary changes to prepare `@alpa/api` to work in production 👇
73 |
74 | ```
75 | cp api/config.example.yml api/config.yml
76 | ```
77 |
78 | ### ⚡ Configuring for production
79 |
80 | Provided example config file is best suitable for development & testing purposes only. We need to make some changes to the config file to make `@alpa/api` suitable for production environments.
81 |
82 | 1. 🔒 **Changing username & password**
83 |
84 | The default username (`{{api.config.auth.username}}`) & password (`{{api.config.auth.password}}`) are extremely insecure. Change both the `auth.username` and `auth.password` fields with better values. And avoid setting the username to commonly guessable values like `admin`, `alpa`, `sudo`, `root` etc.
85 |
86 |
87 |
88 | 2. 🔌 **Changing database connection URL**
89 |
90 | The default database connection URL (`{{api.config.database.connection}}`) is mainly for connecting to an internal Docker container.
91 |
92 | Change the value of `database.connection` field to a Redis database connection URL without a database number pre-selected. Preferably to an empty Redis database exclusively to be used with `@alpa/api`. Using a shared database is also possible with additional configuration.
93 |
94 | > ⚠️ **Warning:** The Redis database must have RedisJSON & RediSearch plugins enabled & working.
95 |
96 |
97 |
98 | 3. 🔑 **Changing server's secret key**
99 |
100 | This secret key is used to sign the JWT authentication tokens, since the default (`{{api.config.server.secret}}`) is already known to everyone. It is insecure to use it.
101 |
102 | Preferably use a password generator to generate a 64 character long random string here.
103 |
104 | > ⚠️ **Warning:** Failing to change the secret key, or using a small secret key will get you into the risk of getting `@alpa/api` hacked. A minimum of 64 characters is recommended.
105 |
106 |
107 |
108 | 4. 🔗 **Changing allowed domains**
109 |
110 | [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) are sent by `@alpa/api` to prevent misuse & accessing the API from unauthorized origins.
111 |
112 | Remove `localhost` entries from `server.cors` field to prevent everyone from self-deploying `@alpa/app` and accessing your instance of `@alpa/api` from their own computer.
113 |
114 | Finally, if you're not using the universal deployment of `@alpa/app` at https://alpa.vercel.app then also remove that entry as a safety measure while adding the full URL of `@alpa/app` hosted by you to allow, programmatic communication from that URL.
115 |
116 | ### ✨ Starting `@alpa/api`
117 |
118 | With the above mentioned changes being done to the configuration file, `@alpa/api` is now ready to be started in a production environment safely.
119 |
120 | On Linux & macOS operating systems run the below command 👇
121 |
122 | ```bash
123 | NODE_ENV=production node api/dist/index.js
124 | ```
125 |
126 | If you're on Windows (_but seriously? why!_ 🤷♂️) then use [cross-env](https://www.npmjs.com/package/cross-env) to set the `NODE_ENV` to production 👇 and start `@alpa/api`:
127 |
128 | ```bash
129 | npx cross-env NODE_ENV=production node api/dist/index.js
130 | ```
131 |
132 | > ℹ️ **Info:** During this process npm may ask you whether to install `cross-env` depending on if you already have it.
133 |
134 | After following the above steps you should be able to login from the configured client and start enjoying **alpa**.
135 |
136 | **If you're still facing issues, refer the [troubleshooting & help section](https://github.com/vsnthdev/alpa#-troubleshooting--help) for further information.**
137 |
138 |
139 |
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/docs/md/app/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: ../README.md
3 | ---
4 |
5 | This project contains a friendly dashboard deployed at https://alpa.vercel.app which can be used to control **alpa's API hosted anywhere**.
6 |
7 | ## 🔮 Tech stack
8 |
9 | | Name | Description |
10 | | --- | --- |
11 | |
**React.js** | Frontend framework of choice. |
12 | |
**Redux** | Store management for React. |
13 | |
**TailwindCSS** | CSS framework for rapid UI building. |
14 | |
**Vite.js** | For bundling JavaScript. |
15 | |
**Vercel** | For deploying frontend. |
16 | |
**nanoid** | For creating short codes. |
17 |
18 | ## 💻 Building & Dev Setup
19 |
20 | You need to be at least on **Node.js v{{nodeVersion}} or above** and follow the below instructions to build this project 👇
21 |
22 | - **STEP 1️⃣** Clone this repository & enter into it (`cd ./alpa`)
23 | - **STEP 2️⃣** Run **`npm install`** to get all dependencies & link projects together
24 | - **STEP 3️⃣** Enter in the project directory (`cd app`)
25 | - **STEP 4️⃣** To build this project run **`npm run build`**
26 |
27 | Upon building `@alpa/app` a production optimized bundle of React.js app is generated in the `dist` folder within the project.
28 |
--------------------------------------------------------------------------------
/docs/md/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: ../README.md
3 | ---
4 |
5 | Reads the TypeScript code in this repository and programmatically generates documentation markdown files.
6 |
7 | ## 🔮 Tech stack
8 |
9 | | Name | Description |
10 | | --- | --- |
11 | |
**Handlebars** | Templating engine to inject values into template markdown files. |
12 | |
**Chokidar** | Watches for file changes and rebuilds docs. |
13 |
14 | ## 💻 Building & Dev Setup
15 |
16 | You need to be at least on **Node.js v{{nodeVersion}} or above** and follow the below instructions to build this project 👇
17 |
18 | - **STEP 1️⃣** Clone this repository & enter into it (`cd ./alpa`)
19 | - **STEP 2️⃣** Run **`npm install`** to get all dependencies & link projects together
20 | - **STEP 3️⃣** Enter in the project directory (`cd docs`)
21 | - **STEP 4️⃣** To build this project run **`npm run build`**
22 |
23 | Upon building `@alpa/docs` will rerender all markdown files within all the projects in this repository.
24 |
25 | > **ℹ️ Info:** You can also run `npm run clean` to delete existing documentation from the project to avoid overwriting & purge dangling documents. While running the `build` script, old docs are first deleted before getting overwritten.
26 |
--------------------------------------------------------------------------------
/docs/media/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsnthdev/alpa/e999519e5a6f2343ac596ac036ac549b9ae66ff5/docs/media/cover.png
--------------------------------------------------------------------------------
/docs/media/logo_dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/media/logo_light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/notes/todo.md:
--------------------------------------------------------------------------------
1 | api
2 | docs/deploy.md
3 | ❌ manual deploy
4 | ❌ docker deploy
5 | ❌ kubernetes deploy
6 | README.md
7 | ✅ configuration
8 | ✅ api routes
9 | ✅ tech stack
10 |
11 | app
12 | README.md
13 | ✅ tech stack
14 |
15 | README.md
16 | ✅ header
17 | ✅ short overview
18 | ✅ features
19 | ✅ why I built it
20 | ✅ quick start
21 | ✅ support & funding
22 | ✅ code & contribution
23 | ✅ building
24 | ✅ license
25 | ✅ footer
26 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@alpa/docs",
3 | "description": "Programmatically ⚡ builds docs 📚 of all projects 📂 under alpa.",
4 | "version": "1.1.1",
5 | "private": true,
6 | "type": "module",
7 | "scripts": {
8 | "clean": "rimraf ./README.md ../README.md ../app/README.md ../api/README.md ../api/docs",
9 | "build": "node --no-warnings --loader ts-node/esm src/build.ts",
10 | "start": "node --no-warnings --loader ts-node/esm src/dev.ts"
11 | },
12 | "dependencies": {
13 | "chokidar": "^3.5.3",
14 | "gray-matter": "^4.0.3",
15 | "handlebars": "^4.7.7",
16 | "mkdirp": "^1.0.4",
17 | "ts-node": "^10.7.0"
18 | },
19 | "devDependencies": {
20 | "@types/handlebars": "^4.1.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/docs/src/build.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Entryfile to build documentation for alpa project.
3 | * Created On 08 March 2022
4 | */
5 |
6 | import dirname from 'es-dirname'
7 | import glob from 'glob'
8 | import path from 'path'
9 |
10 | import getData from './helpers/index.js'
11 | import { log } from './logger.js'
12 | import handleMarkdownFile from './md.js'
13 |
14 | const getMD = async () => {
15 | log.info('Estimating markdown files')
16 | let files = glob.sync(path.join(dirname(), '..', 'md', '**', '**.md'))
17 | files = files.concat(glob.sync(path.join(dirname(), '..', 'md', '**.md')))
18 |
19 | return files
20 | }
21 |
22 | const md = await getMD()
23 | const data = await getData()
24 |
25 | const promises = md.map(file => handleMarkdownFile(file, data))
26 |
27 | await Promise.all(promises)
28 | log.success('Finished generating docs')
29 |
--------------------------------------------------------------------------------
/docs/src/dev.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * A dev command to watch for file changes and re-build changed
3 | * changed files.
4 | * Created On 10 March 2022
5 | */
6 |
7 | import chokidar from 'chokidar'
8 | import dirname from 'es-dirname'
9 | import fs from 'fs'
10 | import path from 'path'
11 |
12 | import getData from './helpers/index.js'
13 | import handleMarkdownFile from './md.js'
14 |
15 | const dir = path.join(dirname(), '..', 'md')
16 | const data = await getData()
17 |
18 | const onChange = (p: string) => {
19 | handleMarkdownFile(p, data)
20 | }
21 |
22 | chokidar
23 | .watch(dir, {
24 | ignored: p => {
25 | const stat = fs.statSync(p)
26 |
27 | // allow directories to be watched
28 | if (stat.isDirectory()) return false
29 |
30 | // only allow markdown files, and rest
31 | // everything should be ignored
32 | return path.parse(p).ext != '.md'
33 | },
34 | })
35 | .on('add', p => onChange(p))
36 | .on('change', p => onChange(p))
37 |
--------------------------------------------------------------------------------
/docs/src/helpers/alpa.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Gets application information for docs to be rendered.
3 | * Created On 09 March 2022
4 | */
5 |
6 | import dirname from 'es-dirname'
7 | import fs from 'fs/promises'
8 | import path from 'path'
9 |
10 | export default async () => {
11 | const packageJSON = await fs.readFile(
12 | path.join(dirname(), '..', '..', '..', 'package.json'),
13 | 'utf-8',
14 | )
15 | const { description, license, engines } = JSON.parse(packageJSON)
16 |
17 | return {
18 | license,
19 | desc: description,
20 | nodeVersion: engines.node.slice(2),
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/docs/src/helpers/api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Returns an object, by reading parts of the @alpa/api project.
3 | * Created On 31 March 2022
4 | */
5 |
6 | import dirname from 'es-dirname'
7 | import fs from 'fs/promises'
8 | import glob from 'glob'
9 | import path from 'path'
10 | import { parse } from 'yaml'
11 |
12 | const getRoutePath = (code: string) => {
13 | const lines = code
14 | .split('export default {')[1]
15 | .split('\n')
16 | .map(line => line.trim())
17 | .filter(line => Boolean(line))
18 |
19 | lines.pop()
20 |
21 | let line = lines.find(line => line.match(/url: /g))
22 | line = line.slice(5, -1).trim()
23 |
24 | const value = eval(line) as string[]
25 | return value.length == 0 ? value[0] : value.join(' & ')
26 | }
27 |
28 | const getRouteMethod = (code: string) => {
29 | const lines = code
30 | .split('export default {')[1]
31 | .split('\n')
32 | .map(line => line.trim())
33 | .filter(line => Boolean(line))
34 |
35 | lines.pop()
36 |
37 | let line = lines.find(line => line.match(/method:/g))
38 | line = line.slice(5, -1).trim()
39 |
40 | const value = eval(line)
41 |
42 | return typeof value == 'string' ? value : value[0]
43 | }
44 |
45 | const getRouteDescription = (code: string) => {
46 | let lines = code.split(' */')[0].split('\n')
47 | lines.shift()
48 | lines = lines.filter(line => Boolean(line))
49 |
50 | return lines[0].slice(2).trim()
51 | }
52 |
53 | const isAuthRequired = (code: string) => {
54 | const lines = code
55 | .split('export default {')[1]
56 | .split('\n')
57 | .map(line => line.trim())
58 | .filter(line => Boolean(line))
59 |
60 | lines.pop()
61 |
62 | if (lines.find(line => line.includes('opts: {'))) {
63 | const others = lines.join(' ').split('opts: {')[1].split('},')[0]
64 | return others.match(/auth/g) ? true : false
65 | } else {
66 | return false
67 | }
68 | }
69 |
70 | const readDefaultConfig = async (api: string): Promise => {
71 | const str = await fs.readFile(path.join(api, 'config.example.yml'), 'utf-8')
72 | return parse(str)
73 | }
74 |
75 | const getApp = async (api: string): Promise => {
76 | const str = await fs.readFile(path.join(api, 'package.json'), 'utf-8')
77 | return JSON.parse(str)
78 | }
79 |
80 | export default async () => {
81 | const api = path.join(dirname(), '..', '..', '..', 'api')
82 | const routeFiles = glob.sync(
83 | path.join(api, 'src', 'server', 'routes', '**', '**', 'index.ts'),
84 | )
85 | const routes = []
86 |
87 | for (const file of routeFiles) {
88 | const code = await fs.readFile(file, 'utf-8')
89 |
90 | routes.push({
91 | path: getRoutePath(code),
92 | method: getRouteMethod(code),
93 | description: getRouteDescription(code),
94 | authRequired: isAuthRequired(code),
95 | })
96 | }
97 |
98 | return {
99 | api: {
100 | routes,
101 | app: await getApp(api),
102 | config: await readDefaultConfig(api),
103 | },
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/docs/src/helpers/generic.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Contains some generic variables.
3 | * Created On 09 March 2022
4 | */
5 |
6 | export default async () => {
7 | const date = new Date()
8 |
9 | return {
10 | year: date.getFullYear(),
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/docs/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Loads all the helpers and returns the data object.
3 | * Created On 09 March 2022
4 | */
5 |
6 | // helpers directory would contain folders containing
7 | // TypeScript modules, that would dynamically interpret
8 | // parts of the codebase to create variables to be used dynamically
9 | // throughout the markdown files
10 |
11 | import chalk from 'chalk'
12 | import dirname from 'es-dirname'
13 | import glob from 'glob'
14 | import path from 'path'
15 |
16 | import { log } from '../logger.js'
17 |
18 | export default async () => {
19 | // get all the files in this directory
20 | log.info('Starting to fetch data')
21 | let files = glob.sync(path.join(dirname(), '*.ts'))
22 | files = files.filter(
23 | file => file != files.find(file => path.parse(file).base == 'index.ts'),
24 | )
25 |
26 | let data = {}
27 |
28 | for (const file of files) {
29 | log.info(`Fetching for ${chalk.gray(path.parse(file).base)}`)
30 | const { default: ts } = await import(`file://${file}`)
31 | data = { ...data, ...(await ts()) }
32 | }
33 |
34 | return data
35 | }
36 |
--------------------------------------------------------------------------------
/docs/src/helpers/projects.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Reads all the projects in this repo to be used within README.md.
3 | * Created On 31 March 2022
4 | */
5 |
6 | import dirname from 'es-dirname'
7 | import fs from 'fs/promises'
8 | import path from 'path'
9 |
10 | const excludes = ['node_modules']
11 |
12 | export default async () => {
13 | const root = path.join(dirname(), '..', '..', '..')
14 | const files = await fs.readdir(root, {
15 | withFileTypes: true,
16 | })
17 |
18 | const projects = files
19 | .filter(file => {
20 | // only allow folder
21 | const outcomes = [
22 | file.isDirectory(),
23 | !excludes.includes(file.name),
24 | file.name.charAt(0) != '.',
25 | ]
26 |
27 | return !outcomes.includes(false)
28 | })
29 | .map(file => file.name)
30 |
31 | const returnable = []
32 |
33 | for (const project of projects) {
34 | const data = await fs.readFile(
35 | path.join(root, project, 'package.json'),
36 | 'utf-8',
37 | )
38 | returnable.push({ ...JSON.parse(data), ...{ projectName: project } })
39 | }
40 |
41 | return {
42 | projects: returnable,
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/docs/src/helpers/twitter.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Fetches my actual Twitter username using alpa & mahat.
3 | * Created On 10 March 2022
4 | */
5 |
6 | import axios from 'axios'
7 | import path from 'path'
8 |
9 | export default async () => {
10 | try {
11 | await axios({
12 | method: 'GET',
13 | url: `https://vas.cx/twitter`,
14 | maxRedirects: 0,
15 | })
16 | } catch ({ response: { status, headers } }) {
17 | if (status == 307)
18 | return {
19 | twitterUsername: path.parse(headers.location).base,
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/docs/src/layout.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Fetches the layout if specified.
3 | * Created On 09 March 2022
4 | */
5 |
6 | import fs from 'fs/promises'
7 | import path from 'path'
8 |
9 | export default async (md: string, context: any, content: string) => {
10 | if (!context.layout) return content
11 |
12 | const layoutFile = path.join(path.dirname(md), context.layout)
13 | const layout = await fs.readFile(layoutFile, 'utf-8')
14 |
15 | return layout
16 | .split('')[0]
17 | .trim()
18 | .concat('\n\n')
19 | .concat(content.trim())
20 | .concat(layout.split('')[1])
21 | .trim()
22 | .concat('\n')
23 | }
24 |
--------------------------------------------------------------------------------
/docs/src/logger.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Initializes itivrutaha logger for @alpa/docs project.
3 | * Created On 08 March 2022
4 | */
5 |
6 | import itivrutaha from 'itivrutaha'
7 |
8 | export const log = await itivrutaha.createNewLogger({
9 | bootLog: false,
10 | shutdownLog: false,
11 | theme: {
12 | string: ':emoji :type :message',
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/docs/src/md.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Processes each single markdown file.
3 | * Created On 09 March 2022
4 | */
5 |
6 | import chalk from 'chalk'
7 | import dirname from 'es-dirname'
8 | import fs from 'fs/promises'
9 | import gm from 'gray-matter'
10 | import handlebars from 'handlebars'
11 | import mkdir from 'mkdirp'
12 | import path from 'path'
13 | import { stringify } from 'yaml'
14 |
15 | import getLayout from './layout.js'
16 | import { log } from './logger.js'
17 |
18 | const getIsIndex = (md: string) => {
19 | const relative = path.relative(path.join(dirname(), '..'), md)
20 | return path.dirname(relative) == 'md' &&
21 | path.basename(relative) == 'README.md'
22 | ? true
23 | : false
24 | }
25 |
26 | export default async (md: string, data: any) => {
27 | // read the file
28 | const src = await fs.readFile(md, 'utf-8')
29 |
30 | // read the front matter
31 | const doc = gm(src)
32 |
33 | // fetch the layout if specified
34 | doc.content = await getLayout(md, doc.data, doc.content)
35 |
36 | // create a YAML template
37 | handlebars.registerHelper('yaml', (data, indent) =>
38 | stringify(data)
39 | .split('\n')
40 | .map(line => ' '.repeat(indent) + line)
41 | .join('\n')
42 | .substring(indent),
43 | )
44 |
45 | // create a handlebars template
46 | const template = handlebars.compile(doc.content, {
47 | noEscape: true,
48 | })
49 |
50 | // render it
51 | const render = template({ ...data, ...{ isIndex: getIsIndex(md) } })
52 |
53 | // write to the destination
54 | const dest = path.join(
55 | dirname(),
56 | '..',
57 | '..',
58 | path.normalize(md).split(path.join('docs', 'md'))[1],
59 | )
60 | await mkdir(path.dirname(dest))
61 | await fs.writeFile(dest, render, 'utf-8')
62 |
63 | // tell the user, we're finished with this file
64 | log.info(`Finished writing ${chalk.gray.underline(dest)}`)
65 | }
66 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": [
3 | "*"
4 | ],
5 | "version": "1.1.1"
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alpa",
3 | "description": "( अल्प ) — A fast ⚡ self-hosted link 🔗 shortener.",
4 | "license": "AGPL-3.0-only",
5 | "type": "module",
6 | "bugs": "https://github.com/vsnthdev/alpa/issues",
7 | "author": {
8 | "name": "Vasanth Developer",
9 | "email": "vasanth@vasanthdeveloper.com",
10 | "url": "https://vsnth.dev"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/vsnthdev/alpa.git"
15 | },
16 | "engines": {
17 | "node": ">=17.4.0"
18 | },
19 | "scripts": {
20 | "postinstall": "lerna bootstrap",
21 | "lint": "eslint --fix --ext cjs,mjs,js,ts,tsx -c ./.eslintrc.cjs .",
22 | "clean": "lerna run clean",
23 | "build": "lerna run --parallel build",
24 | "build:docker": "lerna run --stream build:docker",
25 | "build:docs": "lerna run --loglevel silent --stream --scope @alpa/docs clean && lerna run --loglevel silent --stream --scope @alpa/docs build",
26 | "prepare": "husky install"
27 | },
28 | "dependencies": {
29 | "@vsnthdev/utilities-node": "^2.0.1",
30 | "axios": "^0.25.0",
31 | "chalk": "^5.0.0",
32 | "es-dirname": "^0.1.0",
33 | "glob": "^7.2.0",
34 | "husky": "^7.0.4",
35 | "itivrutaha": "^2.0.13",
36 | "lerna": "^4.0.0",
37 | "yaml": "^2.0.1"
38 | },
39 | "devDependencies": {
40 | "@types/glob": "^7.2.0",
41 | "@types/node": "^17.0.33",
42 | "@typescript-eslint/eslint-plugin": "^5.23.0",
43 | "concurrently": "^7.1.0",
44 | "eslint": "^8.15.0",
45 | "eslint-config-prettier": "^8.5.0",
46 | "eslint-plugin-import": "^2.26.0",
47 | "eslint-plugin-prettier": "^4.0.0",
48 | "eslint-plugin-react": "^7.29.4",
49 | "eslint-plugin-simple-import-sort": "^7.0.0",
50 | "prettier": "^2.6.2",
51 | "rimraf": "^3.0.2",
52 | "typescript": "^4.6.4"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /*
2 | * Prettier run control for alpa project.
3 | * Created On 26 April 2022
4 | */
5 |
6 | module.exports = {
7 | semi: false,
8 | tabWidth: 4,
9 | useTabs: false,
10 | endOfLine: 'lf',
11 | singleQuote: true,
12 | trailingComma: 'all',
13 | bracketSpacing: true,
14 | arrowParens: 'avoid',
15 | parser: 'typescript',
16 | quoteProps: 'as-needed',
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "alwaysStrict": true,
5 | "charset": "UTF-8",
6 | "esModuleInterop": true,
7 | "moduleResolution": "node",
8 | "module": "ESNext",
9 | "newLine": "LF",
10 | "preserveConstEnums": true,
11 | "removeComments": true,
12 | "target": "ESNext",
13 | "incremental": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./api"
6 | },
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------