├── .github
└── workflows
│ └── push-docker-hub.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── __init__.py
├── model_types.py
├── models.py
└── routes.py
├── client
├── .browserslistrc
├── .eslintrc.js
├── .gitignore
├── jest.config.js
├── package-lock.json
├── package.json
├── public
│ ├── apple-touch-icon-114x114.png
│ ├── apple-touch-icon-120x120.png
│ ├── apple-touch-icon-144x144.png
│ ├── apple-touch-icon-152x152.png
│ ├── apple-touch-icon-57x57.png
│ ├── apple-touch-icon-60x60.png
│ ├── apple-touch-icon-72x72.png
│ ├── apple-touch-icon-76x76.png
│ ├── favicon-128.png
│ ├── favicon-16x16.png
│ ├── favicon-196x196.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── favicon.ico
│ ├── index.html
│ ├── mstile-144x144.png
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── mstile-310x310.png
│ └── mstile-70x70.png
├── src
│ ├── App.vue
│ ├── components
│ │ ├── Calendar.vue
│ │ ├── Editor.vue
│ │ ├── Header.vue
│ │ ├── NoteCard.vue
│ │ ├── SimpleTask.vue
│ │ ├── Tags.vue
│ │ └── UnsavedForm.vue
│ ├── interfaces.ts
│ ├── main.ts
│ ├── router
│ │ └── index.ts
│ ├── services
│ │ ├── consts.ts
│ │ ├── eventHub.ts
│ │ ├── localstorage.ts
│ │ ├── notes.ts
│ │ ├── requests.ts
│ │ ├── sharedBuefy.ts
│ │ ├── sidebar.ts
│ │ └── user.ts
│ ├── shims-tsx.d.ts
│ ├── shims-vue.d.ts
│ └── views
│ │ ├── Auth.vue
│ │ ├── Day.vue
│ │ ├── ErrorPage.vue
│ │ ├── Home.vue
│ │ ├── HomeRedirect.vue
│ │ ├── Login.vue
│ │ ├── NewNote.vue
│ │ ├── Note.vue
│ │ ├── PageNotFound.vue
│ │ ├── Search.vue
│ │ ├── Signup.vue
│ │ └── UnauthorizedPage.vue
├── tests
│ └── unit
│ │ └── example.spec.ts
├── tsconfig.json
└── vue.config.js
├── config.py
├── docker-compose.yml
├── migrations
├── README
├── alembic.ini
├── env.py
├── script.py.mako
└── versions
│ ├── 7bd1ee1840ca_meta_table.py
│ ├── 9bd71ed6ccff_remove_unique_constraint_name_.py
│ ├── 9ca5901af374_cleanup.py
│ ├── a477f34dbaa4_initial_config.py
│ ├── ad68860179f2_added_auto_save_column_to_user_table.py
│ └── c440f31aff28_add_unique_constraint_name_for_note_.py
├── requirements.txt
├── run.sh
├── server.py
├── verify_data_migrations.py
└── verify_env.py
/.github/workflows/push-docker-hub.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 | tags:
8 | - '*'
9 | pull_request:
10 | branches:
11 | - 'master'
12 |
13 | jobs:
14 | docker:
15 | runs-on: ubuntu-latest
16 | steps:
17 | -
18 | name: Checkout
19 | uses: actions/checkout@v2
20 | -
21 | name: Docker meta
22 | id: meta
23 | uses: crazy-max/ghaction-docker-meta@v2
24 | with:
25 | images: m0ngr31/dailynotes
26 | tags: |
27 | type=raw,value=latest,enable=${{ endsWith(github.ref, 'master') }}
28 | type=ref,event=tag
29 | flavor: |
30 | latest=false
31 | -
32 | name: Login to DockerHub
33 | if: github.event_name != 'pull_request'
34 | uses: docker/login-action@v1
35 | with:
36 | username: ${{ secrets.DOCKERHUB_USERNAME }}
37 | password: ${{ secrets.DOCKERHUB_TOKEN }}
38 | -
39 | name: Build and push
40 | uses: docker/build-push-action@v2
41 | with:
42 | context: .
43 | push: ${{ github.event_name != 'pull_request' }}
44 | tags: ${{ steps.meta.outputs.tags }}
45 | labels: ${{ steps.meta.outputs.labels }}
46 | -
47 | name: Update repo description
48 | uses: peter-evans/dockerhub-description@v2
49 | with:
50 | username: ${{ secrets.DOCKERHUB_USERNAME }}
51 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
52 | repository: m0ngr31/dailynotes
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/node,flask,python,visualstudiocode
3 | # Edit at https://www.gitignore.io/?templates=node,flask,python,visualstudiocode
4 |
5 | ### Flask ###
6 | instance/*
7 | !instance/.gitignore
8 | .webassets-cache
9 |
10 | ### Flask.Python Stack ###
11 | # Byte-compiled / optimized / DLL files
12 | __pycache__/
13 | *.py[cod]
14 | *$py.class
15 |
16 | # C extensions
17 | *.so
18 |
19 | # Distribution / packaging
20 | .Python
21 | build/
22 | develop-eggs/
23 | dist/
24 | downloads/
25 | eggs/
26 | .eggs/
27 | lib/
28 | lib64/
29 | parts/
30 | sdist/
31 | var/
32 | wheels/
33 | pip-wheel-metadata/
34 | share/python-wheels/
35 | *.egg-info/
36 | .installed.cfg
37 | *.egg
38 | MANIFEST
39 |
40 | # PyInstaller
41 | # Usually these files are written by a python script from a template
42 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
43 | *.manifest
44 | *.spec
45 |
46 | # Installer logs
47 | pip-log.txt
48 | pip-delete-this-directory.txt
49 |
50 | # Unit test / coverage reports
51 | htmlcov/
52 | .tox/
53 | .nox/
54 | .coverage
55 | .coverage.*
56 | .cache
57 | nosetests.xml
58 | coverage.xml
59 | *.cover
60 | .hypothesis/
61 | .pytest_cache/
62 |
63 | # Translations
64 | *.mo
65 | *.pot
66 |
67 | # Scrapy stuff:
68 | .scrapy
69 |
70 | # Sphinx documentation
71 | docs/_build/
72 |
73 | # PyBuilder
74 | target/
75 |
76 | # pyenv
77 | .python-version
78 |
79 | # pipenv
80 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
81 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
82 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
83 | # install all needed dependencies.
84 | #Pipfile.lock
85 |
86 | # celery beat schedule file
87 | celerybeat-schedule
88 |
89 | # SageMath parsed files
90 | *.sage.py
91 |
92 | # Spyder project settings
93 | .spyderproject
94 | .spyproject
95 |
96 | # Rope project settings
97 | .ropeproject
98 |
99 | # Mr Developer
100 | .mr.developer.cfg
101 | .project
102 | .pydevproject
103 |
104 | # mkdocs documentation
105 | /site
106 |
107 | # mypy
108 | .mypy_cache/
109 | .dmypy.json
110 | dmypy.json
111 |
112 | # Pyre type checker
113 | .pyre/
114 |
115 | ### Node ###
116 | # Logs
117 | logs
118 | *.log
119 | npm-debug.log*
120 | yarn-debug.log*
121 | yarn-error.log*
122 | lerna-debug.log*
123 |
124 | # Diagnostic reports (https://nodejs.org/api/report.html)
125 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
126 |
127 | # Runtime data
128 | pids
129 | *.pid
130 | *.seed
131 | *.pid.lock
132 |
133 | # Directory for instrumented libs generated by jscoverage/JSCover
134 | lib-cov
135 |
136 | # Coverage directory used by tools like istanbul
137 | coverage
138 | *.lcov
139 |
140 | # nyc test coverage
141 | .nyc_output
142 |
143 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
144 | .grunt
145 |
146 | # Bower dependency directory (https://bower.io/)
147 | bower_components
148 |
149 | # node-waf configuration
150 | .lock-wscript
151 |
152 | # Compiled binary addons (https://nodejs.org/api/addons.html)
153 | build/Release
154 |
155 | # Dependency directories
156 | node_modules/
157 | jspm_packages/
158 |
159 | # TypeScript v1 declaration files
160 | typings/
161 |
162 | # TypeScript cache
163 | *.tsbuildinfo
164 |
165 | # Optional npm cache directory
166 | .npm
167 |
168 | # Optional eslint cache
169 | .eslintcache
170 |
171 | # Optional REPL history
172 | .node_repl_history
173 |
174 | # Output of 'npm pack'
175 | *.tgz
176 |
177 | # Yarn Integrity file
178 | .yarn-integrity
179 |
180 | # dotenv environment variables file
181 | .env
182 | .env.test
183 |
184 | # parcel-bundler cache (https://parceljs.org/)
185 |
186 | # next.js build output
187 | .next
188 |
189 | # nuxt.js build output
190 | .nuxt
191 |
192 | # rollup.js default build output
193 |
194 | # Uncomment the public line if your project uses Gatsby
195 | # https://nextjs.org/blog/next-9-1#public-directory-support
196 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav
197 | # public
198 |
199 | # Storybook build outputs
200 | .out
201 | .storybook-out
202 |
203 | # vuepress build output
204 | .vuepress/dist
205 |
206 | # Serverless directories
207 | .serverless/
208 |
209 | # FuseBox cache
210 | .fusebox/
211 |
212 | # DynamoDB Local files
213 | .dynamodb/
214 |
215 | # Temporary folders
216 | tmp/
217 | temp/
218 |
219 | ### VisualStudioCode Patch ###
220 | # Ignore all local history of files
221 | .history
222 |
223 | # End of https://www.gitignore.io/api/node,flask,python,visualstudiocode
224 |
225 | .DS_Store
226 | /static
227 |
228 | # local env files
229 | .env.local
230 | .env.*.local
231 |
232 | # Editor directories and files
233 | .idea
234 | .vscode
235 | *.suo
236 | *.ntvs*
237 | *.njsproj
238 | *.sln
239 | *.sw?
240 |
241 | /config
242 | test.sh
243 | test.py
244 |
245 | # Docker volume
246 | /dailynotes-volume
247 |
248 | # Generated exports
249 | export.zip
250 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nikolaik/python-nodejs:python3.8-nodejs12-alpine
2 |
3 | RUN mkdir /app
4 | WORKDIR /app
5 |
6 | COPY . .
7 |
8 | RUN apk add build-base libffi-dev
9 |
10 | RUN \
11 | cd /app && \
12 | pip install -r requirements.txt && \
13 | chmod +x run.sh && \
14 | chmod +x verify_env.py && \
15 | chmod +x verify_data_migrations.py
16 |
17 | RUN \
18 | cd /app/client && \
19 | npm ci && \
20 | npm rebuild node-sass && \
21 | npm run build
22 |
23 | EXPOSE 5000
24 | ENTRYPOINT "./run.sh"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DailyNotes: Daily tasks and notes in Markdown
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Current version: **1.0-beta18**
11 |
12 | ## About
13 | The idea for this app came from using my Hobonichi Techo planner every morning to write down what I needed to accomplish that day & using it for scratching down random thoughts and notes as the day went on. The closest thing I've seen to an app for replacing this system is Noteplan, but I don't use a Mac or an iOS device, and it's not self-hostable, so I decided to write my own.
14 |
15 | Since I had the need for keeping track of to-dos throughout the day, regular Markdown didn't work for me since it doesn't natively support tasks. So as an alternative I'm using Github Flavored Markdown (GFM). I really wanted it to feel like an actual text editor and not just a textbox, so I decided to use CodeMirror to handle all the input. Fira Code is used to provide font ligatures. Some other nice features include code highlighting, text/code folding, and a task list where you can toggle the status of any task from any date or note.
16 |
17 | ## Roadmap
18 | I'd like to try add include at least of some the following features to get to a final v1.0 release:
19 |
20 | - iCal support
21 | - HTML preview (instead of just markdown)
22 | - Kanban board for tasks (and new syntax to attach meta info like swimlane and project for each task)
23 | - Nested tagging
24 |
25 |
26 | ## In Action
27 | Here is some screenshots of what it looks like:
28 |
29 | Main editor:
30 |
31 | 
32 |
33 | Search page:
34 |
35 | 
36 |
37 |
38 | Task list:
39 |
40 | 
41 |
42 | ## Running
43 | The recommended way of running is to pull the image from [Docker Hub](https://hub.docker.com/r/m0ngr31/dailynotes).
44 |
45 | ### Docker Setup
46 |
47 | #### Environment Variables
48 | | Environment Variable | Description | Default |
49 | |---|---|---|
50 | | API_SECRET_KEY | Used to sign API tokens. | Will be generated automatically if not passed in. |
51 | | DATABASE_URI | Connection string for DB. | Will create and use a SQLite DB if not passed in. |
52 | | DB_ENCRYPTION_KEY | Secret key for encrypting data. Length must be a multiple of 16. *Warning*: If changed data will not be able to be decrypted! | Will be generated automatically if not passed in. |
53 | | PREVENT_SIGNUPS | Disable signup form? Anything in this variable will prevent signups. | False |
54 | | BASE_URL | Used when using a subfolder on a reverse proxy | None |
55 | | PUID | User ID (for folder permissions) | None |
56 | | PGID | Group ID (for folder permissions) | None |
57 |
58 |
59 | #### Volumes
60 | | Volume Name | Description |
61 | |---|---|
62 | | /app/config | Used to store DB and environment variables. This is not needed if you pass in all of the above environment variables. |
63 |
64 |
65 | #### Docker Run
66 | By default, the easiest way to get running is:
67 |
68 | ```bash
69 | docker run -p 5000:5000 -v /config_dir:/app/config m0ngr31/dailynotes
70 | ```
71 |
72 | ## Development setup
73 |
74 | ### Installing dependencies
75 | You need Python (works on 2 and 3) and Node >= 8 installed
76 |
77 | ```bash
78 | pip install -r requirements.txt
79 | cd client
80 | npm ci
81 | ```
82 |
83 | ### Creating the environment
84 | You can use the environment variables from above, or you can generate new ones by running the following:
85 |
86 | ```bash
87 | ./verify_env.py
88 | ```
89 |
90 | Keep in mind that since the data is encrypted, if you modify the `DB_ENCRYPTION_KEY` variable, your data will not be accessible anymore.
91 |
92 | ### Running
93 | During development you need to run the client and server simultaneously
94 |
95 | ```bash
96 | ./run.sh
97 | ```
98 |
99 | ```bash
100 | cd client
101 | npm run serve
102 | ```
103 |
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | from config import Config
3 | from flask_sqlalchemy import SQLAlchemy
4 | from flask_migrate import Migrate
5 | from flask_jwt_extended import JWTManager
6 | from flask_argon2 import Argon2
7 |
8 |
9 | app = Flask(__name__,
10 | static_url_path='/static',
11 | static_folder = "../dist/static",
12 | template_folder = "../dist"
13 | )
14 |
15 | app.config.from_object(Config)
16 | db = SQLAlchemy(app)
17 | migrate = Migrate(app, db)
18 | jwt = JWTManager(app)
19 | argon2 = Argon2(app)
20 |
21 | from app import routes, models
22 |
--------------------------------------------------------------------------------
/app/model_types.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.types import TypeDecorator, CHAR
2 | from sqlalchemy.dialects.postgresql import UUID
3 | import uuid
4 |
5 |
6 | class GUID(TypeDecorator):
7 | """Platform-independent GUID type.
8 |
9 | Uses PostgreSQL's UUID type, otherwise uses
10 | CHAR(32), storing as stringified hex values.
11 |
12 | """
13 | impl = CHAR
14 |
15 | def load_dialect_impl(self, dialect):
16 | if dialect.name == 'postgresql':
17 | return dialect.type_descriptor(UUID())
18 | else:
19 | return dialect.type_descriptor(CHAR(32))
20 |
21 | def process_bind_param(self, value, dialect):
22 | if value is None:
23 | return value
24 | elif dialect.name == 'postgresql':
25 | return str(value)
26 | else:
27 | if not isinstance(value, uuid.UUID):
28 | return "%.32x" % uuid.UUID(value).int
29 | else:
30 | # hexstring
31 | return "%.32x" % value.int
32 |
33 | def process_result_value(self, value, dialect):
34 | if value is None:
35 | return value
36 | else:
37 | if not isinstance(value, uuid.UUID):
38 | value = uuid.UUID(value)
39 | return value
40 |
--------------------------------------------------------------------------------
/app/models.py:
--------------------------------------------------------------------------------
1 | from app import app, db
2 | from app.model_types import GUID
3 | from sqlalchemy.sql import func
4 | from sqlalchemy.ext.hybrid import hybrid_property
5 | from sqlalchemy import event
6 | from sqlalchemy.orm.attributes import InstrumentedAttribute
7 | from Crypto.Cipher import AES
8 | import binascii
9 | import uuid
10 | import frontmatter
11 | import re
12 |
13 |
14 | key = app.config['DB_ENCRYPTION_KEY']
15 |
16 |
17 | def aes_encrypt(data):
18 | cipher = AES.new(key, AES.MODE_CFB, key[::-1])
19 | return cipher.encrypt(data)
20 |
21 | def aes_encrypt_old(data):
22 | cipher = AES.new(key)
23 | data = data + (" " * (16 - (len(data) % 16)))
24 | return binascii.hexlify(cipher.encrypt(data))
25 |
26 | def aes_decrypt(data):
27 | # From a new object
28 | if type(data) is InstrumentedAttribute:
29 | return ''
30 |
31 | cipher = AES.new(key, AES.MODE_CFB, key[::-1])
32 |
33 | decrypted = cipher.decrypt(data)
34 |
35 | try:
36 | return decrypted.decode('utf-8')
37 | except:
38 | # Data is in old encryption or it is unencrypted
39 | return aes_decrypt_old(data)
40 |
41 | def aes_decrypt_old(data):
42 | try:
43 | cipher = AES.new(key)
44 | return cipher.decrypt(binascii.unhexlify(data)).rstrip().decode('ascii')
45 | except:
46 | # If data is not encrypted, just return it
47 | return data
48 |
49 |
50 | class User(db.Model):
51 | uuid = db.Column(GUID, primary_key=True, index=True, unique=True, default=lambda: uuid.uuid4())
52 | username = db.Column(db.String(64), unique=True, nullable=False)
53 | password_hash = db.Column(db.String(128), nullable=False)
54 | auto_save = db.Column(db.Boolean, nullable=True)
55 | notes = db.relationship('Note', lazy='dynamic', cascade='all, delete, delete-orphan')
56 | meta = db.relationship('Meta', lazy='dynamic', cascade='all, delete, delete-orphan')
57 |
58 | def __repr__(self):
59 | return ''.format(self.uuid)
60 |
61 |
62 | class Meta(db.Model):
63 | uuid = db.Column(GUID, primary_key=True, index=True, unique=True, default=lambda: uuid.uuid4())
64 | user_id = db.Column(GUID, db.ForeignKey('user.uuid'), nullable=False)
65 | note_id = db.Column(GUID, db.ForeignKey('note.uuid'), nullable=False)
66 | name_encrypted = db.Column('name', db.String)
67 | name_compare = db.Column(db.String)
68 | kind = db.Column(db.String)
69 |
70 | @hybrid_property
71 | def name(self):
72 | return aes_decrypt(self.name_encrypted)
73 |
74 | @name.setter
75 | def name(self, value):
76 | self.name_encrypted = aes_encrypt(value)
77 |
78 | def __repr__(self):
79 | return ' '.format(self.uuid)
80 |
81 | @property
82 | def serialize(self):
83 | return {
84 | 'uuid': self.uuid,
85 | 'name': self.name,
86 | 'kind': self.kind,
87 | 'note_id': self.note_id,
88 | }
89 |
90 |
91 | class Note(db.Model):
92 | uuid = db.Column(GUID, primary_key=True, index=True, unique=True, default=lambda: uuid.uuid4())
93 | user_id = db.Column(GUID, db.ForeignKey('user.uuid'), nullable=False)
94 | data = db.Column(db.String)
95 | title = db.Column(db.String(128), nullable=False)
96 | date = db.Column(db.DateTime(timezone=True), server_default=func.now())
97 | is_date = db.Column(db.Boolean, default=False)
98 | meta = db.relationship('Meta', lazy='dynamic', cascade='all, delete, delete-orphan')
99 |
100 | @hybrid_property
101 | def text(self):
102 | return aes_decrypt(self.data)
103 |
104 | @text.setter
105 | def text(self, value):
106 | self.data = aes_encrypt(value)
107 |
108 | @hybrid_property
109 | def name(self):
110 | return aes_decrypt(self.title)
111 |
112 | @name.setter
113 | def name(self, value):
114 | self.title = aes_encrypt(value)
115 |
116 | def __repr__(self):
117 | return ''.format(self.uuid)
118 |
119 | @property
120 | def serialize(self):
121 | return {
122 | 'uuid': self.uuid,
123 | 'data': self.text,
124 | 'title': self.name,
125 | 'date': self.date,
126 | 'is_date': self.is_date,
127 | }
128 |
129 |
130 | # Update title automatically
131 | def before_change_note(mapper, connection, target):
132 | title = None
133 |
134 | data = frontmatter.loads(target.text)
135 |
136 | if isinstance(data.get('title'), str) and len(data.get('title')) > 0:
137 | title = data.get('title')
138 |
139 | if title and not target.is_date:
140 | target.name = title
141 |
142 |
143 | # Handle changes to tasks, projects, and tags
144 | def after_change_note(mapper, connection, target):
145 | tags = []
146 | projects = []
147 |
148 | data = frontmatter.loads(target.text)
149 |
150 | if isinstance(data.get('tags'), list):
151 | tags = list(set([x.replace(',', '\,') for x in data.get('tags')]))
152 | elif isinstance(data.get('tags'), str):
153 | tags = list(set(map(str.strip, data['tags'].split(','))))
154 | tags = [x for x in tags if x]
155 |
156 | if isinstance(data.get('projects'), list):
157 | projects = list(set([x.replace(',', '\,') for x in data.get('projects')]))
158 | elif isinstance(data.get('projects'), str):
159 | projects = list(set(map(str.strip, data['projects'].split(','))))
160 | projects = [x for x in projects if x]
161 |
162 | tasks = re.findall("- \[[x| ]\] .*$", data.content, re.MULTILINE)
163 |
164 | existing_tags = []
165 | existing_projects = []
166 | existing_tasks = []
167 |
168 | metas = Meta.query.filter_by(note_id=target.uuid).all()
169 |
170 | for meta in metas:
171 | if meta.kind == 'tag':
172 | existing_tags.append(meta)
173 | elif meta.kind == 'project':
174 | existing_projects.append(meta)
175 | elif meta.kind == 'task':
176 | existing_tasks.append(meta)
177 |
178 | for tag in existing_tags:
179 | if tag.name not in tags:
180 | connection.execute(
181 | 'DELETE FROM meta WHERE uuid = ?',
182 | '{}'.format(tag.uuid).replace('-', '')
183 | )
184 | else:
185 | tags.remove(tag.name)
186 |
187 | for tag in tags:
188 | connection.execute(
189 | 'INSERT INTO meta (uuid, user_id, note_id, name, kind) VALUES (?, ?, ?, ?, ?)',
190 | '{}'.format(uuid.uuid4()).replace('-', ''),
191 | '{}'.format(target.user_id).replace('-', ''),
192 | '{}'.format(target.uuid).replace('-', ''),
193 | aes_encrypt(tag),
194 | 'tag'
195 | )
196 |
197 | for project in existing_projects:
198 | if project.name not in projects:
199 | connection.execute(
200 | 'DELETE FROM meta WHERE uuid = ?',
201 | '{}'.format(project.uuid).replace('-', '')
202 | )
203 | else:
204 | projects.remove(project.name)
205 |
206 | for project in projects:
207 | connection.execute(
208 | 'INSERT INTO meta (uuid, user_id, note_id, name, kind) VALUES (?, ?, ?, ?, ?)',
209 | '{}'.format(uuid.uuid4()).replace('-', ''),
210 | '{}'.format(target.user_id).replace('-', ''),
211 | '{}'.format(target.uuid).replace('-', ''),
212 | aes_encrypt(project),
213 | 'project'
214 | )
215 |
216 | for task in existing_tasks:
217 | if task.name not in tasks:
218 | connection.execute(
219 | 'DELETE FROM meta WHERE uuid = ?',
220 | '{}'.format(task.uuid).replace('-', '')
221 | )
222 | else:
223 | tasks.remove(task.name)
224 |
225 | for task in tasks:
226 | encrypted_task = aes_encrypt(task)
227 |
228 | connection.execute(
229 | 'INSERT INTO meta (uuid, user_id, note_id, name, name_compare, kind) VALUES (?, ?, ?, ?, ?, ?)',
230 | '{}'.format(uuid.uuid4()).replace('-', ''),
231 | '{}'.format(target.user_id).replace('-', ''),
232 | '{}'.format(target.uuid).replace('-', ''),
233 | encrypted_task,
234 | encrypted_task,
235 | 'task'
236 | )
237 |
238 | def before_update_task(mapper, connection, target):
239 | if target.kind != 'task':
240 | return
241 |
242 | if target.name_encrypted == target.name_compare:
243 | return
244 |
245 | note = Note.query.get(target.note_id)
246 |
247 | if not note:
248 | return
249 |
250 | note_data = aes_encrypt(note.text.replace(aes_decrypt(target.name_compare), target.name))
251 |
252 | connection.execute(
253 | 'UPDATE note SET data = ? WHERE uuid = ?',
254 | note_data,
255 | '{}'.format(note.uuid).replace('-', '')
256 | )
257 |
258 | target.name_compare = target.name_encrypted
259 |
260 |
261 | event.listen(Note, 'before_insert', before_change_note)
262 | event.listen(Note, 'before_update', before_change_note)
263 | event.listen(Note, 'after_insert', after_change_note)
264 | event.listen(Note, 'after_update', after_change_note)
265 | event.listen(Meta, 'before_update', before_update_task)
266 |
--------------------------------------------------------------------------------
/app/routes.py:
--------------------------------------------------------------------------------
1 | import os
2 | import zipfile
3 |
4 | from app import app, db, argon2
5 | from app.models import User, Note, Meta, aes_encrypt, aes_encrypt_old
6 | from flask import render_template, request, jsonify, abort, send_file
7 | from flask_jwt_extended import jwt_required, create_access_token, get_jwt_identity
8 |
9 |
10 | @app.route('/api/sign-up', methods=['POST'])
11 | def sign_up():
12 | if app.config['PREVENT_SIGNUPS']:
13 | abort(400)
14 |
15 | req = request.get_json()
16 | username = req.get('username')
17 | password = req.get('password')
18 |
19 | if not username or not password:
20 | abort(400)
21 |
22 | password_hash = argon2.generate_password_hash(password)
23 |
24 | new_user = User(username=username.lower(), password_hash=password_hash)
25 | db.session.add(new_user)
26 | db.session.commit()
27 |
28 | access_token = create_access_token(identity=username)
29 | return jsonify(access_token=access_token), 200
30 |
31 |
32 | @app.route('/api/login', methods=['POST'])
33 | def login():
34 | req = request.get_json()
35 | username = req.get('username')
36 | password = req.get('password')
37 |
38 | if not username or not password:
39 | abort(400)
40 |
41 | user = User.query.filter_by(username=username.lower()).first()
42 |
43 | if not user:
44 | return jsonify({"msg": "Bad username or password"}), 401
45 |
46 | if not argon2.check_password_hash(user.password_hash, password):
47 | return jsonify({"msg": "Bad username or password"}), 401
48 |
49 | access_token = create_access_token(identity=username)
50 | return jsonify(access_token=access_token), 200
51 |
52 |
53 | @app.route('/api/save_day', methods=['PUT'])
54 | @jwt_required()
55 | def save_day():
56 | req = request.get_json()
57 | title = req.get('title')
58 | data = req.get('data', '')
59 |
60 | if not title:
61 | abort(400)
62 |
63 | username = get_jwt_identity()
64 |
65 | if not username:
66 | abort(401)
67 |
68 | user = User.query.filter_by(username=username.lower()).first()
69 |
70 | if not user:
71 | abort(400)
72 |
73 | enc_date = aes_encrypt(title)
74 | note = user.notes.filter_by(title=enc_date).first()
75 |
76 | if not Note:
77 | # Check old encryption
78 | enc_date = aes_encrypt_old(title)
79 | note = user.notes.filter_by(title=enc_date).first()
80 | if not note:
81 | note = Note(user_id=user.uuid, name=title, text=data, is_date=True)
82 | else:
83 | note.text = data
84 |
85 | db.session.add(note)
86 | db.session.flush()
87 | db.session.commit()
88 |
89 | return jsonify(note=note.serialize), 200
90 |
91 |
92 | @app.route('/api/create_note', methods=['POST'])
93 | @jwt_required()
94 | def create_note():
95 | req = request.get_json()
96 | data = req.get('data', '')
97 |
98 | if not data:
99 | abort(400)
100 |
101 | username = get_jwt_identity()
102 |
103 | if not username:
104 | abort(401)
105 |
106 | user = User.query.filter_by(username=username.lower()).first()
107 |
108 | if not user:
109 | abort(400)
110 |
111 | note = Note(user_id=user.uuid, text=data)
112 |
113 | db.session.add(note)
114 | db.session.flush()
115 | db.session.commit()
116 |
117 | return jsonify(note=note.serialize), 200
118 |
119 |
120 | @app.route('/api/save_task', methods=['PUT'])
121 | @jwt_required()
122 | def save_task():
123 | req = request.get_json()
124 | uuid = req.get('uuid')
125 | name = req.get('name')
126 |
127 | if not uuid or not name:
128 | abort(400)
129 |
130 | username = get_jwt_identity()
131 |
132 | if not username:
133 | abort(401)
134 |
135 | user = User.query.filter_by(username=username.lower()).first()
136 |
137 | if not user:
138 | abort(400)
139 |
140 | task = user.meta.filter_by(uuid=uuid).first()
141 |
142 | if not task:
143 | abort(400)
144 |
145 | task.name = name
146 |
147 | db.session.add(task)
148 | db.session.flush()
149 | db.session.commit()
150 |
151 | return jsonify({}), 200
152 |
153 |
154 | @app.route('/api/save_note', methods=['PUT'])
155 | @jwt_required()
156 | def save_note():
157 | req = request.get_json()
158 | uuid = req.get('uuid')
159 | data = req.get('data', '')
160 |
161 | if not uuid:
162 | abort(400)
163 |
164 | username = get_jwt_identity()
165 |
166 | if not username:
167 | abort(401)
168 |
169 | user = User.query.filter_by(username=username.lower()).first()
170 |
171 | if not user:
172 | abort(400)
173 |
174 | note = user.notes.filter_by(uuid=uuid).first()
175 |
176 | if not note:
177 | abort(400)
178 |
179 | note.text = data
180 |
181 | db.session.add(note)
182 | db.session.flush()
183 | db.session.commit()
184 |
185 | return jsonify(note=note.serialize), 200
186 |
187 |
188 | @app.route('/api/delete_note/', methods=['DELETE'])
189 | @jwt_required()
190 | def delete_note(uuid):
191 | if not uuid:
192 | abort(400)
193 |
194 | username = get_jwt_identity()
195 |
196 | if not username:
197 | abort(401)
198 |
199 | user = User.query.filter_by(username=username.lower()).first()
200 |
201 | if not user:
202 | abort(400)
203 |
204 | note = user.notes.filter_by(uuid=uuid).first()
205 |
206 | if not note:
207 | abort(400)
208 |
209 | db.session.delete(note)
210 | db.session.commit()
211 |
212 | return jsonify({}), 200
213 |
214 |
215 | @app.route('/api/refresh_jwt', methods=['GET'])
216 | @jwt_required()
217 | def refresh_jwt():
218 | username = get_jwt_identity()
219 |
220 | if not username:
221 | abort(401)
222 |
223 | access_token = create_access_token(identity=username)
224 | return jsonify(token=access_token), 200
225 |
226 |
227 | @app.route('/api/note', methods=['GET'])
228 | @jwt_required()
229 | def get_note():
230 | uuid = request.args.get('uuid')
231 |
232 | if not uuid:
233 | abort(400)
234 |
235 | username = get_jwt_identity()
236 | user = User.query.filter_by(username=username.lower()).first()
237 |
238 | if not user:
239 | abort(400)
240 |
241 | note = user.notes.filter_by(uuid=uuid).first()
242 |
243 | if not note:
244 | abort(400)
245 |
246 | return jsonify(note=note.serialize), 200
247 |
248 |
249 | @app.route('/api/date', methods=['GET'])
250 | @jwt_required()
251 | def get_date():
252 | date = request.args.get('date')
253 |
254 | if not date:
255 | abort(400)
256 |
257 | username = get_jwt_identity()
258 | user = User.query.filter_by(username=username.lower()).first()
259 |
260 | if not user:
261 | abort(400)
262 |
263 | ret_note = {
264 | 'title': date,
265 | 'data': '---\ntags: \nprojects: \n---\n\n',
266 | 'is_date': True,
267 | 'user_id': user.uuid
268 | }
269 |
270 | date_enc = aes_encrypt(date)
271 | note = user.notes.filter_by(title=date_enc, is_date=True).first()
272 |
273 | if not note:
274 | # Check old encryption
275 | date_enc = aes_encrypt_old(date)
276 | note = user.notes.filter_by(title=date_enc, is_date=True).first()
277 |
278 | if note:
279 | ret_note = note.serialize
280 |
281 | return jsonify(day=ret_note), 200
282 |
283 |
284 | @app.route('/api/events', methods=['GET'])
285 | @jwt_required()
286 | def cal_events():
287 | username = get_jwt_identity()
288 | user = User.query.filter_by(username=username.lower()).first()
289 |
290 | if not user:
291 | abort(400)
292 |
293 | # TODO: Only do current month or something
294 | notes = user.notes.filter_by(is_date=True).all()
295 |
296 | return jsonify(events=[x.name for x in notes]), 200
297 |
298 |
299 | @app.route('/api/sidebar', methods=['GET'])
300 | @jwt_required()
301 | def sidebar_data():
302 | username = get_jwt_identity()
303 | user = User.query.filter_by(username=username.lower()).first()
304 |
305 | if not user:
306 | abort(400)
307 |
308 | notes = sorted([a.serialize for a in user.notes.filter_by(is_date=False).all()], key=lambda note: note['title'].lower())
309 | tags = sorted(set([a.name for a in user.meta.filter_by(kind="tag").all()]), key=lambda s: s.lower())
310 | projects = sorted(set([a.name for a in user.meta.filter_by(kind="project").all()]), key=lambda s: s.lower())
311 | tasks = sorted([a.serialize for a in user.meta.filter_by(kind="task").all()], key=lambda task: task['note_id'])
312 | auto_save = user.auto_save
313 |
314 | return jsonify(tags=tags,projects=projects,notes=notes,tasks=tasks,auto_save=auto_save), 200
315 |
316 |
317 | @app.route('/api/toggle_auto_save', methods=['POST'])
318 | @jwt_required()
319 | def toggle_auto_save():
320 | req = request.get_json()
321 | auto_save = req.get('auto_save', False)
322 |
323 | username = get_jwt_identity()
324 |
325 | if not username:
326 | abort(401)
327 |
328 | user = User.query.filter_by(username=username.lower()).first()
329 |
330 | if not user:
331 | abort(400)
332 |
333 | user.auto_save = auto_save
334 |
335 | db.session.add(user)
336 | db.session.flush()
337 | db.session.commit()
338 |
339 | return jsonify({}), 200
340 |
341 |
342 | @app.route('/api/search', methods=['POST'])
343 | @jwt_required()
344 | def search():
345 | req = request.get_json()
346 | selected_search = req.get('selected', '')
347 | search_string = req.get('search', '')
348 |
349 | if not selected_search or not search_string or not len(search_string) > 0:
350 | abort(400)
351 |
352 | if selected_search not in ['project', 'tag', 'search']:
353 | abort(400)
354 |
355 | username = get_jwt_identity()
356 |
357 | if not username:
358 | abort(401)
359 |
360 | user = User.query.filter_by(username=username.lower()).first()
361 |
362 | if not user:
363 | abort(400)
364 |
365 | matched_notes = []
366 |
367 | if selected_search == 'project':
368 | all_projects = user.meta.filter_by(kind="project").all()
369 |
370 | for project in all_projects:
371 | if search_string.lower() in project.name.lower():
372 | matched_notes.append(project.note_id)
373 |
374 | elif selected_search == 'tag':
375 | all_tags = user.meta.filter_by(kind="tag").all()
376 |
377 | for tag in all_tags:
378 | if search_string.lower() in tag.name.lower():
379 | matched_notes.append(tag.note_id)
380 |
381 | elif selected_search == 'search':
382 | all_notes = user.notes.all()
383 |
384 | for note in all_notes:
385 | if search_string.lower() in note.text.lower():
386 | matched_notes.append(note.uuid)
387 |
388 | filtered_notes = Note.query.filter(Note.uuid.in_(matched_notes)).all()
389 | notes = []
390 |
391 | for note in filtered_notes:
392 | cleaned_note = note.serialize
393 | cleaned_note['tags'] = sorted(set([x.name for x in note.meta.filter_by(kind="tag").all()]), key=lambda s: s.lower())
394 | cleaned_note['projects'] = sorted(set([x.name for x in note.meta.filter_by(kind="project").all()]), key=lambda s: s.lower())
395 | notes.append(cleaned_note)
396 |
397 | sorted_nodes = sorted(notes, key=lambda s: s['title'].lower())
398 |
399 | return jsonify(notes=sorted_nodes), 200
400 |
401 |
402 | @app.route('/api/export')
403 | @jwt_required()
404 | def export():
405 | username = get_jwt_identity()
406 | user = User.query.filter_by(username=username.lower()).first()
407 |
408 | if not user:
409 | abort(400)
410 |
411 | zip_location = app.config['EXPORT_FILE']
412 | zf = zipfile.ZipFile(zip_location, mode='w')
413 | os.chmod(zip_location, 0o755)
414 | notes = user.notes
415 | for note in notes:
416 | ret_note = note.serialize
417 | zf.writestr(ret_note['title'] + '.md', ret_note['data'], zipfile.ZIP_DEFLATED)
418 | print(ret_note)
419 | zf.close()
420 |
421 | rval = send_file(zip_location, as_attachment=True)
422 | os.remove(zip_location)
423 | return rval
424 |
425 |
426 | @app.route('/', defaults={'path': ''})
427 | @app.route('/')
428 | def catch_all(path):
429 | return render_template("index.html")
430 |
--------------------------------------------------------------------------------
/client/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/client/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"],
7 | rules: {
8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
10 | },
11 | parserOptions: {
12 | parser: "@typescript-eslint/parser"
13 | },
14 | overrides: [
15 | {
16 | files: [
17 | "**/__tests__/*.{j,t}s?(x)",
18 | "**/tests/unit/**/*.spec.{j,t}s?(x)"
19 | ],
20 | env: {
21 | jest: true
22 | }
23 | }
24 | ]
25 | };
26 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | # local env files
6 | .env.local
7 | .env.*.local
8 |
9 | # Log files
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Editor directories and files
15 | .idea
16 | .vscode
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
--------------------------------------------------------------------------------
/client/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "@vue/cli-plugin-unit-jest/presets/typescript"
3 | };
4 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "daily-notes",
3 | "version": "1.0.0-beta.15",
4 | "private": true,
5 | "license": "MIT",
6 | "repository": {
7 | "url": "https://github.com/m0ngr31/DailyNotes"
8 | },
9 | "readme": "https://github.com/m0ngr31/DailyNotes/blob/master/README.md",
10 | "scripts": {
11 | "serve": "vue-cli-service serve",
12 | "build": "vue-cli-service build",
13 | "test:unit": "vue-cli-service test:unit",
14 | "lint": "vue-cli-service lint"
15 | },
16 | "dependencies": {
17 | "@fortawesome/fontawesome-free": "^5.12.0",
18 | "axios": "^0.21.1",
19 | "buefy": "^0.8.9",
20 | "bulmaswatch": "^0.7.5",
21 | "codemirror": "^5.50.2",
22 | "date-fns": "^2.8.1",
23 | "lodash": "^4.17.15",
24 | "vue": "^2.6.10",
25 | "vue-class-component": "^7.0.2",
26 | "vue-masonry-css": "^1.0.3",
27 | "vue-meta": "^2.3.1",
28 | "vue-property-decorator": "^8.3.0",
29 | "vue-router": "^3.1.3"
30 | },
31 | "devDependencies": {
32 | "@types/codemirror": "0.0.82",
33 | "@types/jest": "^24.0.19",
34 | "@types/lodash": "^4.14.149",
35 | "@types/node": "^14.11.1",
36 | "@vue/cli-plugin-eslint": "^4.1.0",
37 | "@vue/cli-plugin-router": "^4.1.0",
38 | "@vue/cli-plugin-typescript": "^4.1.0",
39 | "@vue/cli-plugin-unit-jest": "^4.1.0",
40 | "@vue/cli-service": "^4.1.0",
41 | "@vue/eslint-config-prettier": "^5.0.0",
42 | "@vue/eslint-config-typescript": "^4.0.0",
43 | "@vue/test-utils": "1.0.0-beta.29",
44 | "eslint": "^5.16.0",
45 | "eslint-plugin-prettier": "^3.1.1",
46 | "eslint-plugin-vue": "^5.0.0",
47 | "lint-staged": "^9.5.0",
48 | "node-sass": "^4.13.0",
49 | "prettier": "^1.19.1",
50 | "sass-loader": "^8.0.0",
51 | "typescript": "~3.5.3",
52 | "vue-template-compiler": "^2.6.10"
53 | },
54 | "gitHooks": {
55 | "pre-commit": "lint-staged"
56 | },
57 | "lint-staged": {
58 | "*.{js,vue,ts}": [
59 | "vue-cli-service lint",
60 | "git add"
61 | ]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/client/public/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/client/public/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/client/public/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/client/public/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/client/public/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/client/public/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/client/public/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/client/public/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/client/public/favicon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/favicon-128.png
--------------------------------------------------------------------------------
/client/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/favicon-16x16.png
--------------------------------------------------------------------------------
/client/public/favicon-196x196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/favicon-196x196.png
--------------------------------------------------------------------------------
/client/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/favicon-32x32.png
--------------------------------------------------------------------------------
/client/public/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/favicon-96x96.png
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | DailyNotes
30 |
31 |
32 |
33 | We're sorry but DailyNotes doesn't work properly without JavaScript enabled. Please enable it to continue.
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/client/public/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/mstile-144x144.png
--------------------------------------------------------------------------------
/client/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/mstile-150x150.png
--------------------------------------------------------------------------------
/client/public/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/mstile-310x150.png
--------------------------------------------------------------------------------
/client/public/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/mstile-310x310.png
--------------------------------------------------------------------------------
/client/public/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/m0ngr31/DailyNotes/c3064dc6c7cf919422c515f6635b042a3fbfbda0/client/public/mstile-70x70.png
--------------------------------------------------------------------------------
/client/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
38 |
39 |
151 |
--------------------------------------------------------------------------------
/client/src/components/Calendar.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/src/components/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
148 |
149 |
--------------------------------------------------------------------------------
/client/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
96 |
97 |
98 |
204 |
205 |
--------------------------------------------------------------------------------
/client/src/components/NoteCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ parsedTitle }}
4 |
5 |
6 | Tags
7 |
8 | {{tag}}
9 |
10 |
11 | Projects
12 |
13 | {{project}}
14 |
15 |
16 |
17 |
18 |
62 |
63 |
--------------------------------------------------------------------------------
/client/src/components/SimpleTask.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ taskName }}
5 |
6 |
7 |
8 |
9 |
74 |
75 |
--------------------------------------------------------------------------------
/client/src/components/Tags.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Tags
8 |
9 |
10 | {{tag}}
11 |
12 |
13 |
14 |
Projects
15 |
16 |
17 | {{project}}
18 |
19 |
20 |
21 |
Notes
22 |
23 |
24 | {{note.title}}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
52 |
53 |
--------------------------------------------------------------------------------
/client/src/components/UnsavedForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
31 |
32 |
47 |
48 |
49 |
50 |
51 |
52 |
95 |
--------------------------------------------------------------------------------
/client/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface IHeaderOptions {
2 | title: string;
3 | showDateNavs?: boolean;
4 | showDelete?: boolean;
5 | hideCreate?: boolean;
6 | saveDisabled?: boolean;
7 | saveFn?: () => Promise;
8 | deleteFn?: () => Promise;
9 | }
10 |
11 | export interface INote {
12 | uuid?: string | null;
13 | data: string;
14 | title?: string;
15 | is_date?: boolean;
16 | tags?: string;
17 | projects?: string;
18 | }
19 |
20 | export interface IMeta {
21 | uuid: string;
22 | name: string;
23 | note_id: string;
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/main.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Buefy from 'buefy';
3 | import VueMeta from 'vue-meta';
4 | import VueMasonry from 'vue-masonry-css';
5 |
6 | import App from './App.vue';
7 | import router from './router';
8 |
9 | Vue.config.productionTip = false;
10 |
11 | Vue.use(Buefy, {
12 | defaultIconPack: 'fas',
13 | });
14 | Vue.use(VueMeta);
15 | Vue.use(VueMasonry);
16 |
17 | new Vue({
18 | router,
19 | render: h => h(App)
20 | }).$mount('#app');
21 |
--------------------------------------------------------------------------------
/client/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import VueRouter from "vue-router";
3 |
4 | import Home from "../views/Home.vue";
5 |
6 | import PageNotFound from '../views/PageNotFound.vue';
7 | import UnauthorizedPage from '../views/UnauthorizedPage.vue';
8 | import ErrorPage from '../views/ErrorPage.vue';
9 |
10 | import Day from '../views/Day.vue';
11 | import Note from '../views/Note.vue';
12 | import NewNote from '../views/NewNote.vue';
13 | import Search from '../views/Search.vue';
14 | import HomeRedirect from '../views/HomeRedirect.vue';
15 |
16 | import Auth from '../views/Auth.vue';
17 | import Login from '../views/Login.vue';
18 | import Signup from '../views/Signup.vue';
19 |
20 | import {getToken} from '../services/user';
21 | import SidebarInst from '../services/sidebar';
22 |
23 |
24 | Vue.use(VueRouter);
25 |
26 | const routes = [
27 | {
28 | path: '/auth',
29 | component: Auth,
30 | meta: { auth: false },
31 | children: [
32 | {
33 | path: '',
34 | alias: 'login',
35 | name: 'Login',
36 | component: Login
37 | },
38 | {
39 | path: 'sign-up',
40 | name: 'Sign Up',
41 | component: Signup
42 | }
43 | ]
44 | },
45 | {
46 | path: '/',
47 | component: Home,
48 | meta: { auth: true },
49 | children: [
50 | {
51 | path: '',
52 | name: 'Home Redirect',
53 | component: HomeRedirect
54 | },
55 | {
56 | path: 'date/:id',
57 | name: 'day-id',
58 | component: Day
59 | },
60 | {
61 | path: 'note/:uuid',
62 | name: 'note-id',
63 | component: Note
64 | },
65 | {
66 | path: 'new-note',
67 | name: 'new-note',
68 | component: NewNote
69 | },
70 | {
71 | path: 'search',
72 | name: 'search',
73 | component: Search
74 | }
75 | ]
76 | },
77 | {
78 | path: '/page-not-found',
79 | name: '404',
80 | component: PageNotFound
81 | },
82 | {
83 | path: '/not-authorized',
84 | name: '401',
85 | component: UnauthorizedPage
86 | },
87 | {
88 | path: '/error',
89 | name: 'Error',
90 | component: ErrorPage
91 | },
92 | {
93 | path: '*',
94 | redirect: '/page-not-found'
95 | }
96 |
97 | ];
98 |
99 | const router = new VueRouter({
100 | mode: 'history',
101 | linkActiveClass: 'active',
102 | base: process.env.BASE_URL,
103 | routes
104 | });
105 |
106 | router.beforeEach(async (to, from, next) => {
107 | const currentUser = getToken();
108 | const requiresAuth = to.matched.some(record => record.meta.auth);
109 |
110 | if (requiresAuth && !currentUser) {
111 | await next({name: 'Login', query: {from: to.path}});
112 | } else if (!requiresAuth && currentUser) {
113 | await next({name: 'Home Redirect'});
114 | } else {
115 | if (requiresAuth && to.name !== 'day-id') {
116 | SidebarInst.date = null;
117 | }
118 |
119 | if (requiresAuth && to.name !== 'search') {
120 | SidebarInst.searchString = '';
121 | SidebarInst.selectedSearch = '';
122 | }
123 |
124 | await next();
125 | }
126 | });
127 |
128 | export default router;
129 |
--------------------------------------------------------------------------------
/client/src/services/consts.ts:
--------------------------------------------------------------------------------
1 | export const newNote = `---\ntitle: \ntags: \nprojects: \n---\n\n`;
2 | export const newDay = `---\ntags: \nprojects: \n---\n\n`;
3 |
--------------------------------------------------------------------------------
/client/src/services/eventHub.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | const eventHub = new Vue();
4 |
5 | export default eventHub;
6 |
--------------------------------------------------------------------------------
/client/src/services/localstorage.ts:
--------------------------------------------------------------------------------
1 | export function getItemOrDefault(key: string, _default: any = null) {
2 | if (!typeof(Storage)) {
3 | return _default;
4 | }
5 |
6 | const value = localStorage.getItem(key);
7 |
8 | if (!value){
9 | return _default;
10 | }
11 |
12 | return JSON.parse(value);
13 | }
14 |
15 | export function setItem(key: string, data: any) {
16 | if (!typeof(Storage)) {
17 | return;
18 | }
19 |
20 | localStorage.setItem(key, JSON.stringify(data));
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/services/notes.ts:
--------------------------------------------------------------------------------
1 | import {Requests} from './requests';
2 |
3 | import {INote} from '../interfaces';
4 |
5 | export const NoteService = {
6 | /**
7 | * Get the note for the active day.
8 | *
9 | * @param date Date in 'MM-dd-yyyy' format
10 | */
11 | getDate: async (date: string): Promise => {
12 | if (!date) {
13 | Promise.reject();
14 | }
15 |
16 | try {
17 | const res = await Requests.get('/date', {
18 | date
19 | });
20 |
21 | if (res.data && res.data.day) {
22 | return res.data.day as INote;
23 | }
24 |
25 | throw new Error('no matching data');
26 | } catch (e) {
27 | throw new Error(e);
28 | }
29 | },
30 |
31 | /**
32 | * Get the selected note.
33 | *
34 | * @param uuid
35 | */
36 | getNote: async (uuid: string): Promise => {
37 | if (!uuid) {
38 | Promise.reject();
39 | }
40 |
41 | try {
42 | const res = await Requests.get('/note', {
43 | uuid,
44 | });
45 |
46 | return res.data.note as INote;
47 | } catch (e) {
48 | throw new Error(e);
49 | }
50 | },
51 |
52 | createNote: async (noteData: INote): Promise => {
53 | const res = await Requests.post('/create_note', noteData);
54 | return res.data.note;
55 | },
56 |
57 | /**
58 | * Get a list of all the notes
59 | */
60 | getNotes: async (): Promise => {
61 | try {
62 | const res = await Requests.get('/notes');
63 |
64 | return res.data as INote[];
65 | } catch (e) {
66 | throw new Error(e);
67 | }
68 | },
69 |
70 | /**
71 | * Save an individual date
72 | *
73 | * @param noteData INote object
74 | */
75 | saveDay: async (noteData: INote): Promise => {
76 | const res = await Requests.put('/save_day', noteData);
77 | return res.data.note;
78 | },
79 |
80 | /**
81 | * Save an individual note
82 | *
83 | * @param noteData INote object
84 | */
85 | saveNote: async (noteData: INote): Promise => {
86 | const res = await Requests.put('/save_note', noteData);
87 | return res.data.note;
88 | },
89 |
90 | /**
91 | * Delete an individual note
92 | */
93 | deleteNote: async (uuid: string): Promise => {
94 | await Requests.delete(`/delete_note/${uuid}`);
95 | },
96 |
97 | /**
98 | * Exports all notes to a zip file and downloads
99 | */
100 | exportNotes: async (): Promise => {
101 | Requests.download("/export", "export.zip");
102 | }
103 | };
104 |
--------------------------------------------------------------------------------
/client/src/services/requests.ts:
--------------------------------------------------------------------------------
1 | import axios, {AxiosPromise} from 'axios';
2 |
3 | import {getToken, clearToken, setToken} from './user';
4 | import router from '../router/index';
5 | import {SharedBuefy} from './sharedBuefy';
6 |
7 | axios.defaults.baseURL = process.env.VUE_APP_BASE_URL
8 | ? `${process.env.VUE_APP_BASE_URL}/api`
9 | : '/api';
10 |
11 | axios.interceptors.request.use(config => {
12 | // Get token
13 | const token = getToken();
14 |
15 | if (token) {
16 | config.headers.common['Authorization'] = `Bearer ${token}`;
17 | }
18 |
19 | return config;
20 | }, (error) => {
21 | return Promise.reject(error);
22 | });
23 |
24 | axios.interceptors.response.use(res => res, async err => {
25 | if (err.response && (err.response.status === 403 || err.response.status === 401 || err.response.status === 422)) {
26 | // Logout
27 | clearToken();
28 |
29 | // Preventing dialogs from firing
30 | SharedBuefy.preventDialog = true;
31 |
32 | if (router.currentRoute.path.indexOf('/auth') !== 0) {
33 | try {
34 | (SharedBuefy.notifications as any).open({
35 | duration: 5000,
36 | message: 'Session expired. Logging out.',
37 | position: 'is-top',
38 | type: 'is-warning'
39 | });
40 | } catch(e) {}
41 | }
42 |
43 | try {
44 | (SharedBuefy.activeDialog as any).close();
45 | } catch(e) {}
46 |
47 | router.push({ name: 'Login' });
48 | }
49 |
50 | // Prevent dialogs from firing on network errors caused by expired JWTs
51 | setTimeout(() => {
52 | SharedBuefy.preventDialog = false;
53 | }, 1000);
54 |
55 | return Promise.reject(err);
56 | });
57 |
58 | export const Requests = {
59 | post: (url: string, data: any): AxiosPromise => {
60 | return axios.post(url, data);
61 | },
62 |
63 | get: (url: string, data?: any): AxiosPromise => {
64 | return axios.get(url, { params: data || {} });
65 | },
66 |
67 | put: (url: string, data: any): AxiosPromise => {
68 | return axios.put(url, data);
69 | },
70 |
71 | delete: (url: string): AxiosPromise => {
72 | return axios.delete(url);
73 | },
74 |
75 | download: (url: string, filename: string): void => {
76 | axios({
77 | url: url, // File URL Goes Here
78 | method: "GET",
79 | responseType: "blob"
80 | }).then(res => {
81 | var FILE = window.URL.createObjectURL(new Blob([res.data]));
82 | var docUrl = document.createElement("a");
83 | docUrl.href = FILE;
84 | docUrl.setAttribute("download", filename);
85 | document.body.appendChild(docUrl);
86 | docUrl.click();
87 | document.body.removeChild(docUrl);
88 | });
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/client/src/services/sharedBuefy.ts:
--------------------------------------------------------------------------------
1 | // Stupid hack to give access to Buefy outside of Vue components
2 | export const SharedBuefy = {
3 | activeDialog: null,
4 | dialog: null,
5 | notifications: null,
6 | preventDialog: false,
7 | openConfirmDialog: function(options: any = {}) {
8 | if (this.preventDialog) {
9 | return;
10 | }
11 |
12 | if (this.activeDialog) {
13 | try {
14 | (this.activeDialog as any).close();
15 | this.activeDialog = null;
16 | } catch (e) {}
17 | }
18 |
19 | this.activeDialog = (this.dialog as any).confirm(options);
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/client/src/services/sidebar.ts:
--------------------------------------------------------------------------------
1 | import parse from 'date-fns/parse';
2 | import formatISO from 'date-fns/formatISO';
3 | import {Route} from 'vue-router';
4 | import _ from 'lodash';
5 |
6 | import {Requests} from './requests';
7 |
8 | import router from '../router';
9 |
10 | import {INote, IMeta} from '../interfaces';
11 |
12 | class SidebarSerivce {
13 | public hide: boolean = false;
14 | public events: any[] = [];
15 | public tags: string[] = [];
16 | public tasks: IMeta[] = [];
17 | public projects: string[] = [];
18 | public notes: INote[] = [];
19 | public calLoading: boolean = false;
20 | public autoSave: boolean = false;
21 | public date: any = null;
22 | public sidebarLoading: boolean = false;
23 | public searchLoading: boolean = false;
24 | public selectedSearch: string = '';
25 | public searchString: any = '';
26 | public filteredNotes: any[] = [];
27 |
28 | /**
29 | * Updates the active date on the calendar picker. This is throttled
30 | * so that it doesn't get overwhelmed.
31 | *
32 | * @param $route A VueRouter Route object
33 | */
34 | public updateDate = _.throttle(($route: Route) => {
35 | if (!$route.params || !$route.params.id) {
36 | this.date = null;
37 | return;
38 | }
39 |
40 | try {
41 | this.date = parse($route.params.id, 'MM-dd-yyyy', new Date());
42 | } catch (e) {
43 | // Reset date
44 | this.date = null;
45 | }
46 | }, 250, {trailing: true, leading: false});
47 |
48 | /**
49 | * Get the event indicators for the calendar
50 | */
51 | public async getEvents(): Promise {
52 | if (this.calLoading) {
53 | return;
54 | }
55 |
56 | let date = new Date();
57 |
58 | if (this.date) {
59 | date = this.date;
60 | }
61 |
62 | this.calLoading = true;
63 |
64 | try {
65 | const res = await Requests.get('/events', {
66 | date: formatISO(date),
67 | });
68 |
69 | if (res && res.data && res.data.events) {
70 | this.events = _.map(res.data.events, event => {
71 | return {
72 | date: parse(event, 'MM-dd-yyyy', new Date()),
73 | };
74 | });
75 | }
76 | } catch (e) {}
77 |
78 | this.calLoading = false;
79 | }
80 |
81 | /**
82 | * Gets the tags, projects, and notes information loaded into the sidebar.
83 | *
84 | * @param showLoad Boolean to show the loading indicator or not
85 | */
86 | public async getSidebarInfo(showLoad = false): Promise {
87 | if (this.sidebarLoading) {
88 | return;
89 | }
90 |
91 | if (showLoad) {
92 | this.sidebarLoading = true;
93 | }
94 |
95 | try {
96 | const res = await Requests.get('/sidebar');
97 |
98 | if (res && res.data) {
99 | this.tags = res.data.tags;
100 | this.tasks = res.data.tasks;
101 | this.projects = res.data.projects;
102 | this.notes = res.data.notes;
103 | this.autoSave = res.data.auto_save;
104 | }
105 |
106 | if (this.selectedSearch.length && this.searchString.length) {
107 | this.searchNotes();
108 | }
109 | } catch (e) {}
110 |
111 | this.sidebarLoading = false;
112 | }
113 |
114 | public async searchNotes() {
115 | if (this.searchLoading) {
116 | return;
117 | }
118 |
119 | this.searchLoading = true;
120 |
121 | try {
122 | const res = await Requests.post('/search', {
123 | selected: this.selectedSearch,
124 | search: this.searchString,
125 | });
126 |
127 | if (res && res.data) {
128 | this.filteredNotes = res.data.notes || [];
129 | }
130 | } catch (e) {}
131 |
132 | this.searchLoading = false;
133 |
134 | router.push({name: 'search', query: {[this.selectedSearch]: this.searchString}}).catch(err => {});
135 | }
136 |
137 | public async saveTaskProgress(name: string, uuid: string) {
138 | try {
139 | await Requests.put('/save_task', {name, uuid});
140 | this.getSidebarInfo();
141 | } catch (e) {}
142 | }
143 |
144 | public async toggleAutoSave(autoSave: boolean) {
145 | try {
146 | await Requests.post('/toggle_auto_save', {auto_save: autoSave});
147 | this.getSidebarInfo();
148 | } catch (e) {}
149 | }
150 | }
151 |
152 | // Make it a singleton
153 | const SidebarInst = new SidebarSerivce();
154 |
155 | export default SidebarInst;
156 |
--------------------------------------------------------------------------------
/client/src/services/user.ts:
--------------------------------------------------------------------------------
1 | import {Requests} from './requests';
2 |
3 | const AUTH_TOKEN = 'dn-token';
4 |
5 | export function getToken() {
6 | if (typeof(Storage)) {
7 | return localStorage.getItem(AUTH_TOKEN);
8 | }
9 |
10 | return null;
11 | }
12 |
13 | export async function setToken(token: string) {
14 | if (typeof(Storage)) {
15 | localStorage.setItem(AUTH_TOKEN, token);
16 | }
17 | }
18 |
19 | export function clearToken() {
20 | if (typeof(Storage)) {
21 | localStorage.removeItem(AUTH_TOKEN)
22 | }
23 | }
24 |
25 | export async function updateJWT() {
26 | try {
27 | const res = await Requests.get('/refresh_jwt');
28 |
29 | if (!res || !res.data || !res.data.token) {
30 | throw new Error('no token');
31 | }
32 |
33 | setToken(res.data.token);
34 | } catch (e) {}
35 | }
--------------------------------------------------------------------------------
/client/src/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from "vue";
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.vue" {
2 | import Vue from "vue";
3 | export default Vue;
4 | }
5 |
6 | declare module 'vue-masonry-css';
7 |
--------------------------------------------------------------------------------
/client/src/views/Auth.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
23 |
24 |
32 |
33 |
55 |
--------------------------------------------------------------------------------
/client/src/views/Day.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
281 |
282 |
289 |
--------------------------------------------------------------------------------
/client/src/views/ErrorPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
There was an error. Please go back and try again.
7 |
8 |
9 |
10 |
11 |
12 |
20 |
--------------------------------------------------------------------------------
/client/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
29 |
30 |
31 |
80 |
81 |
--------------------------------------------------------------------------------
/client/src/views/HomeRedirect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/client/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{errMsg}}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Login
12 |
13 |
14 |
15 |
16 |
17 |
98 |
--------------------------------------------------------------------------------
/client/src/views/NewNote.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
138 |
139 |
146 |
--------------------------------------------------------------------------------
/client/src/views/Note.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
222 |
223 |
230 |
--------------------------------------------------------------------------------
/client/src/views/PageNotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Page Not Found. Please go back and try again.
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/src/views/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Project
8 | Tag
9 | Text
10 |
11 |
12 |
13 | Search
14 |
15 |
16 |
17 |
18 |
22 | There are no notes that match that query.
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
105 |
106 |
--------------------------------------------------------------------------------
/client/src/views/Signup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{errMsg}}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Sign Up
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/client/src/views/UnauthorizedPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
You are unauthorized to view this page. Please go back.
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/tests/unit/example.spec.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount } from "@vue/test-utils";
2 | import HelloWorld from "@/components/HelloWorld.vue";
3 |
4 | describe("HelloWorld.vue", () => {
5 | it("renders props.msg when passed", () => {
6 | const msg = "new message";
7 | const wrapper = shallowMount(HelloWorld, {
8 | propsData: { msg }
9 | });
10 | expect(wrapper.text()).toMatch(msg);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "experimentalDecorators": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "sourceMap": true,
13 | "baseUrl": ".",
14 | "types": [
15 | "webpack-env",
16 | "jest"
17 | ],
18 | "paths": {
19 | "@/*": [
20 | "src/*"
21 | ]
22 | },
23 | "lib": [
24 | "esnext",
25 | "dom",
26 | "dom.iterable",
27 | "scripthost"
28 | ]
29 | },
30 | "include": [
31 | "src/**/*.ts",
32 | "src/**/*.tsx",
33 | "src/**/*.vue",
34 | "tests/**/*.ts",
35 | "tests/**/*.tsx"
36 | ],
37 | "exclude": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/client/vue.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | process.env.VUE_APP_PREVENT_SIGNUPS = process.env.PREVENT_SIGNUPS ? true : '';
5 | process.env.VUE_APP_BASE_URL = process.env.BASE_URL ? VUE_APP_BASE_URL : '';
6 |
7 | module.exports = {
8 | lintOnSave: false,
9 | outputDir: path.resolve(__dirname, '../dist'),
10 | assetsDir: 'static',
11 | devServer: {
12 | proxy: {
13 | '^/api': {
14 | target: 'http://localhost:5000',
15 | changeOrigin: true
16 | },
17 | }
18 | },
19 | configureWebpack: {
20 | plugins: [
21 | new webpack.ContextReplacementPlugin(
22 | /date\-fns[\/\\]/,
23 | new RegExp('[/\\\\\](en)[/\\\\\]')
24 | )
25 | ]
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | import datetime
3 |
4 | basedir = os.path.abspath(os.path.dirname(__file__))
5 |
6 | class Config(object):
7 | JWT_SECRET_KEY = os.environ.get('API_SECRET_KEY')
8 | DB_ENCRYPTION_KEY = os.environ.get('DB_ENCRYPTION_KEY')
9 | PREVENT_SIGNUPS = os.environ.get('PREVENT_SIGNUPS', False)
10 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir + '/config', 'app.db')
11 | SQLALCHEMY_TRACK_MODIFICATIONS = False
12 | JWT_ACCESS_TOKEN_EXPIRES = datetime.timedelta(days=7)
13 | EXPORT_FILE = os.path.join(basedir, 'config', 'export.zip')
14 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 |
5 | server:
6 | build: .
7 | ports:
8 | - 5000:5000
9 | volumes:
10 | - ./dailynotes-volume:/app/config
11 | environment:
12 | API_SECRET_KEY: CHANGE_THIS_WITH_SECURE_PASSWORD
13 | DB_ENCRYPTION_KEY: CHANGE_THIS_WITH_SECURE_PASSWORD
--------------------------------------------------------------------------------
/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/migrations/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | # file_template = %%(rev)s_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 |
3 | import logging
4 | from logging.config import fileConfig
5 |
6 | from sqlalchemy import engine_from_config
7 | from sqlalchemy import pool
8 |
9 | from alembic import context
10 |
11 | # this is the Alembic Config object, which provides
12 | # access to the values within the .ini file in use.
13 | config = context.config
14 |
15 | # Interpret the config file for Python logging.
16 | # This line sets up loggers basically.
17 | fileConfig(config.config_file_name)
18 | logger = logging.getLogger('alembic.env')
19 |
20 | # add your model's MetaData object here
21 | # for 'autogenerate' support
22 | # from myapp import mymodel
23 | # target_metadata = mymodel.Base.metadata
24 | from flask import current_app
25 | config.set_main_option(
26 | 'sqlalchemy.url', current_app.config.get(
27 | 'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
28 | target_metadata = current_app.extensions['migrate'].db.metadata
29 |
30 | # other values from the config, defined by the needs of env.py,
31 | # can be acquired:
32 | # my_important_option = config.get_main_option("my_important_option")
33 | # ... etc.
34 |
35 |
36 | def run_migrations_offline():
37 | """Run migrations in 'offline' mode.
38 |
39 | This configures the context with just a URL
40 | and not an Engine, though an Engine is acceptable
41 | here as well. By skipping the Engine creation
42 | we don't even need a DBAPI to be available.
43 |
44 | Calls to context.execute() here emit the given string to the
45 | script output.
46 |
47 | """
48 | url = config.get_main_option("sqlalchemy.url")
49 | context.configure(
50 | url=url, target_metadata=target_metadata, literal_binds=True
51 | )
52 |
53 | with context.begin_transaction():
54 | context.run_migrations()
55 |
56 |
57 | def run_migrations_online():
58 | """Run migrations in 'online' mode.
59 |
60 | In this scenario we need to create an Engine
61 | and associate a connection with the context.
62 |
63 | """
64 |
65 | # this callback is used to prevent an auto-migration from being generated
66 | # when there are no changes to the schema
67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
68 | def process_revision_directives(context, revision, directives):
69 | if getattr(config.cmd_opts, 'autogenerate', False):
70 | script = directives[0]
71 | if script.upgrade_ops.is_empty():
72 | directives[:] = []
73 | logger.info('No changes in schema detected.')
74 |
75 | connectable = engine_from_config(
76 | config.get_section(config.config_ini_section),
77 | prefix='sqlalchemy.',
78 | poolclass=pool.NullPool,
79 | )
80 |
81 | with connectable.connect() as connection:
82 | context.configure(
83 | connection=connection,
84 | target_metadata=target_metadata,
85 | # render_as_batch=True,
86 | process_revision_directives=process_revision_directives,
87 | user_module_prefix="app.model_types.",
88 | **current_app.extensions['migrate'].configure_args
89 | )
90 |
91 | with context.begin_transaction():
92 | context.run_migrations()
93 |
94 |
95 | if context.is_offline_mode():
96 | run_migrations_offline()
97 | else:
98 | run_migrations_online()
99 |
--------------------------------------------------------------------------------
/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import app.model_types
11 | ${imports if imports else ""}
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = ${repr(up_revision)}
15 | down_revision = ${repr(down_revision)}
16 | branch_labels = ${repr(branch_labels)}
17 | depends_on = ${repr(depends_on)}
18 |
19 |
20 | def upgrade():
21 | ${upgrades if upgrades else "pass"}
22 |
23 |
24 | def downgrade():
25 | ${downgrades if downgrades else "pass"}
26 |
--------------------------------------------------------------------------------
/migrations/versions/7bd1ee1840ca_meta_table.py:
--------------------------------------------------------------------------------
1 | """Meta Table
2 |
3 | Revision ID: 7bd1ee1840ca
4 | Revises: 9ca5901af374
5 | Create Date: 2020-01-30 01:13:40.069188
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import app.model_types
11 |
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = '7bd1ee1840ca'
15 | down_revision = '9ca5901af374'
16 | branch_labels = None
17 | depends_on = None
18 |
19 |
20 | def upgrade():
21 | # ### commands auto generated by Alembic - please adjust! ###
22 | op.create_table('meta',
23 | sa.Column('uuid', app.model_types.GUID(), nullable=False),
24 | sa.Column('user_id', app.model_types.GUID(), nullable=False),
25 | sa.Column('note_id', app.model_types.GUID(), nullable=False),
26 | sa.Column('name', sa.String(), nullable=True),
27 | sa.Column('name_compare', sa.String(), nullable=True),
28 | sa.Column('kind', sa.String(), nullable=True),
29 | sa.ForeignKeyConstraint(['note_id'], ['note.uuid'], ),
30 | sa.ForeignKeyConstraint(['user_id'], ['user.uuid'], ),
31 | sa.PrimaryKeyConstraint('uuid')
32 | )
33 | op.create_index(op.f('ix_meta_uuid'), 'meta', ['uuid'], unique=True)
34 | # ### end Alembic commands ###
35 |
36 |
37 | def downgrade():
38 | # ### commands auto generated by Alembic - please adjust! ###
39 | op.drop_index(op.f('ix_meta_uuid'), table_name='meta')
40 | op.drop_table('meta')
41 | # ### end Alembic commands ###
42 |
--------------------------------------------------------------------------------
/migrations/versions/9bd71ed6ccff_remove_unique_constraint_name_.py:
--------------------------------------------------------------------------------
1 | """Remove unique constraint name for note title
2 |
3 | Revision ID: 9bd71ed6ccff
4 | Revises: c440f31aff28
5 | Create Date: 2021-02-27 15:10:54.803203
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import app.model_types
11 |
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = '9bd71ed6ccff'
15 | down_revision = 'c440f31aff28'
16 | branch_labels = None
17 | depends_on = None
18 |
19 |
20 | def upgrade():
21 | with op.batch_alter_table('note', schema=None) as batch_op:
22 | batch_op.drop_constraint('title_uniq', type_='unique')
23 |
24 |
25 | def downgrade():
26 | pass
27 |
--------------------------------------------------------------------------------
/migrations/versions/9ca5901af374_cleanup.py:
--------------------------------------------------------------------------------
1 | """cleanup
2 |
3 | Revision ID: 9ca5901af374
4 | Revises: a477f34dbaa4
5 | Create Date: 2020-01-28 20:44:00.184324
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import app.model_types
11 |
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = '9ca5901af374'
15 | down_revision = 'a477f34dbaa4'
16 | branch_labels = None
17 | depends_on = None
18 |
19 |
20 | def upgrade():
21 | # ### commands auto generated by Alembic - please adjust! ###
22 | with op.batch_alter_table('note', schema=None) as batch_op:
23 | batch_op.drop_column('projects')
24 | batch_op.drop_column('tags')
25 |
26 | # ### end Alembic commands ###
27 |
28 |
29 | def downgrade():
30 | # ### commands auto generated by Alembic - please adjust! ###
31 | with op.batch_alter_table('note', schema=None) as batch_op:
32 | batch_op.add_column(sa.Column('tags', sa.VARCHAR(), nullable=True))
33 | batch_op.add_column(sa.Column('projects', sa.VARCHAR(), nullable=True))
34 |
35 | # ### end Alembic commands ###
36 |
--------------------------------------------------------------------------------
/migrations/versions/a477f34dbaa4_initial_config.py:
--------------------------------------------------------------------------------
1 | """Initial config
2 |
3 | Revision ID: a477f34dbaa4
4 | Revises:
5 | Create Date: 2020-01-07 21:31:19.007718
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import app.model_types
11 |
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = 'a477f34dbaa4'
15 | down_revision = None
16 | branch_labels = None
17 | depends_on = None
18 |
19 |
20 | def upgrade():
21 | # ### commands auto generated by Alembic - please adjust! ###
22 | op.create_table('user',
23 | sa.Column('uuid', app.model_types.GUID(), nullable=False),
24 | sa.Column('username', sa.String(length=64), nullable=False),
25 | sa.Column('password_hash', sa.String(length=128), nullable=False),
26 | sa.PrimaryKeyConstraint('uuid'),
27 | sa.UniqueConstraint('username')
28 | )
29 | op.create_index(op.f('ix_user_uuid'), 'user', ['uuid'], unique=True)
30 | op.create_table('note',
31 | sa.Column('uuid', app.model_types.GUID(), nullable=False),
32 | sa.Column('tags', sa.String(), nullable=True),
33 | sa.Column('projects', sa.String(), nullable=True),
34 | sa.Column('user_id', app.model_types.GUID(), nullable=False),
35 | sa.Column('data', sa.String(), nullable=True),
36 | sa.Column('title', sa.String(length=128), nullable=False),
37 | sa.Column('date', sa.DateTime(timezone=True), server_default=sa.text(u'(CURRENT_TIMESTAMP)'), nullable=True),
38 | sa.Column('is_date', sa.Boolean(), nullable=True),
39 | sa.ForeignKeyConstraint(['user_id'], ['user.uuid'], ),
40 | sa.PrimaryKeyConstraint('uuid'),
41 | sa.UniqueConstraint('title')
42 | )
43 | op.create_index(op.f('ix_note_uuid'), 'note', ['uuid'], unique=True)
44 | # ### end Alembic commands ###
45 |
46 |
47 | def downgrade():
48 | # ### commands auto generated by Alembic - please adjust! ###
49 | op.drop_index(op.f('ix_note_uuid'), table_name='note')
50 | op.drop_table('note')
51 | op.drop_index(op.f('ix_user_uuid'), table_name='user')
52 | op.drop_table('user')
53 | # ### end Alembic commands ###
54 |
--------------------------------------------------------------------------------
/migrations/versions/ad68860179f2_added_auto_save_column_to_user_table.py:
--------------------------------------------------------------------------------
1 | """Added auto_save column to User table
2 |
3 | Revision ID: ad68860179f2
4 | Revises: 9bd71ed6ccff
5 | Create Date: 2021-11-23 17:33:25.117589
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import app.model_types
11 |
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = 'ad68860179f2'
15 | down_revision = '9bd71ed6ccff'
16 | branch_labels = None
17 | depends_on = None
18 |
19 |
20 | def upgrade():
21 | # ### commands auto generated by Alembic - please adjust! ###
22 | with op.batch_alter_table('user', schema=None) as batch_op:
23 | batch_op.add_column(sa.Column('auto_save', sa.Boolean(), nullable=True))
24 |
25 | # ### end Alembic commands ###
26 |
27 |
28 | def downgrade():
29 | # ### commands auto generated by Alembic - please adjust! ###
30 | with op.batch_alter_table('user', schema=None) as batch_op:
31 | batch_op.drop_column('auto_save')
32 |
33 | # ### end Alembic commands ###
34 |
--------------------------------------------------------------------------------
/migrations/versions/c440f31aff28_add_unique_constraint_name_for_note_.py:
--------------------------------------------------------------------------------
1 | """Add named unique constraint name for note title
2 |
3 | Revision ID: c440f31aff28
4 | Revises: 7bd1ee1840ca
5 | Create Date: 2021-02-27 08:59:27.748780
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import app.model_types
11 |
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = 'c440f31aff28'
15 | down_revision = '7bd1ee1840ca'
16 | branch_labels = None
17 | depends_on = None
18 |
19 |
20 | def upgrade():
21 | with op.batch_alter_table('note', schema=None) as batch_op:
22 | batch_op.create_unique_constraint('title_uniq', ['title'])
23 |
24 |
25 | def downgrade():
26 | with op.batch_alter_table('note', schema=None) as batch_op:
27 | pass
28 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | flask_sqlalchemy
3 | flask-migrate
4 | gunicorn
5 | flask-jwt-extended>4
6 | flask-argon2
7 | python-frontmatter
8 | pycrypto
--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if test -f "./config/.env"; then
4 | . ./config/.env
5 | fi
6 |
7 | ./verify_env.py
8 |
9 | if test -f "./config/.env"; then
10 | . ./config/.env
11 | fi
12 |
13 | export FLASK_APP=server.py
14 |
15 | flask db upgrade
16 |
17 | ./verify_data_migrations.py
18 |
19 | exec gunicorn server:app -b 0.0.0.0:5000
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | from app import app
2 |
--------------------------------------------------------------------------------
/verify_data_migrations.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from app import app, db
4 | from app.models import Note, Meta
5 |
6 | # Setup Flask context
7 | ctx = app.test_request_context()
8 | ctx.push()
9 |
10 | def main():
11 | needs_migration = False
12 |
13 | first_note = Note.query.first()
14 | meta = Meta.query.first()
15 |
16 | if meta or not first_note or first_note.text is not first_note.data:
17 | return
18 |
19 | # Notes need to be migrated
20 | notes = Note.query.all()
21 |
22 | for note in notes:
23 | # Trigger a change
24 | note.text = note.data + ''
25 | note.name = note.title + ''
26 | db.session.add(note)
27 |
28 | db.session.commit()
29 |
30 | main()
31 |
--------------------------------------------------------------------------------
/verify_env.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import random
4 | import string
5 | import os
6 | import os.path
7 |
8 |
9 | def gen_random(str_len):
10 | return ''.join(random.choice(string.hexdigits) for x in range(str_len))
11 |
12 |
13 | def main():
14 | # Don't need to create config folder if env vars are already set
15 | if os.getenv('API_SECRET_KEY', None) and os.getenv('DATABASE_URI', None) and os.getenv('DB_ENCRYPTION_KEY'):
16 | return
17 |
18 | API_SECRET_KEY = os.getenv('API_SECRET_KEY', None)
19 | ENCRYPTION_KEY = os.getenv('DB_ENCRYPTION_KEY', None)
20 |
21 | if not API_SECRET_KEY:
22 | SECRET_KEY = "export API_SECRET_KEY=\"{}\"".format(gen_random(48))
23 | else:
24 | SECRET_KEY = "export API_SECRET_KEY=\"{}\"".format(API_SECRET_KEY)
25 |
26 | if not ENCRYPTION_KEY:
27 | ENCRYPTION_KEY = "export DB_ENCRYPTION_KEY=\"{}\"".format(gen_random(16))
28 | else:
29 | ENCRYPTION_KEY = "export DB_ENCRYPTION_KEY=\"{}\"".format(ENCRYPTION_KEY)
30 |
31 | if not os.path.isdir('./config'):
32 | os.makedirs('./config')
33 |
34 | f = open('./config/.env', "w")
35 | f.writelines([SECRET_KEY, "\n", ENCRYPTION_KEY])
36 | f.close()
37 |
38 | main()
39 |
--------------------------------------------------------------------------------