Compare commits
9 Commits
master
...
cloud-save
| Author | SHA1 | Date | |
|---|---|---|---|
| cd8046ed87 | |||
| 2db8f4dafb | |||
| acdff74039 | |||
| bb9c9c7587 | |||
| 7e93d9374b | |||
|
|
4023b3fec8 | ||
|
|
7458750053 | ||
|
|
b741964c3a | ||
| 17aa8c75b1 |
58
.drone.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: frontend
|
||||
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: bulid
|
||||
image: node:16
|
||||
environment:
|
||||
SSH_KEY:
|
||||
from_secret: SSH_KEY
|
||||
commands:
|
||||
- git clone -b cloud-saves https://git.radner.ru/anatolykopyl/worktime.git
|
||||
- cd worktime/frontend
|
||||
- npm install
|
||||
- npm run build
|
||||
- mkdir ~/.ssh
|
||||
- echo "$SSH_KEY" > ~/.ssh/id_rsa
|
||||
- chmod 400 ~/.ssh/id_rsa
|
||||
- scp -o StrictHostKeyChecking=no -r dist/* webmaster@worktime.anatolykopyl.ru:~/www/worktime.anatolykopyl.ru
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- cloud-saves
|
||||
|
||||
---
|
||||
|
||||
kind: pipeline
|
||||
type: ssh
|
||||
name: backend
|
||||
|
||||
server:
|
||||
host: worktime.anatolykopyl.ru
|
||||
user: webmaster
|
||||
ssh_key:
|
||||
from_secret: SSH_KEY
|
||||
|
||||
clone:
|
||||
disable: true
|
||||
|
||||
steps:
|
||||
- name: fetch remote
|
||||
commands:
|
||||
- cd /home/webmaster/worktime && git fetch --all && git reset --hard origin/cloud-saves
|
||||
- name: build docker
|
||||
commands:
|
||||
- docker build --tag worktime:latest /home/webmaster/worktime/backend
|
||||
- name: restart docker
|
||||
commands:
|
||||
- docker stop worktime || true
|
||||
- docker rm worktime || true
|
||||
- docker run --name worktime -p 3003:3000 -d worktime
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- cloud-saves
|
||||
33
.github/workflows/deploy.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: CI and DI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set-up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "14.2.x"
|
||||
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
|
||||
- name: Deploy
|
||||
uses: crazy-max/ghaction-github-pages@v1
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
7
.gitignore
vendored
@@ -1,11 +1,9 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
# env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
@@ -21,3 +19,4 @@ pnpm-debug.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
3
backend/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
PORT=3000
|
||||
MONGODB_URI=
|
||||
SECRET=
|
||||
7
backend/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM denoland/deno:1.21.2
|
||||
EXPOSE 3000
|
||||
WORKDIR /app
|
||||
USER deno
|
||||
COPY . .
|
||||
RUN deno cache ./src/index.ts
|
||||
CMD ["run", "--allow-all", "./src/index.ts"]
|
||||
6
backend/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Worktime Backend
|
||||
|
||||
To start dev server:
|
||||
```
|
||||
$ denon dev
|
||||
```
|
||||
9
backend/scripts.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": {
|
||||
"cmd": "./src/index.ts",
|
||||
"desc": "Run the development server.",
|
||||
"allow": ["net", "env", "read"]
|
||||
}
|
||||
}
|
||||
}
|
||||
7
backend/src/authorized.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default async (ctx: any, next: Function) => {
|
||||
const userId = await ctx.state.session.get('userId');
|
||||
if (!userId) {
|
||||
ctx.throw(403);
|
||||
}
|
||||
await next();
|
||||
}
|
||||
16
backend/src/getProviderId.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default async function getProviderId(token: string, authProvider: string) {
|
||||
switch (authProvider) {
|
||||
case 'yandex':
|
||||
return getYandexId(token);
|
||||
}
|
||||
}
|
||||
|
||||
async function getYandexId(token: string) {
|
||||
const baseUrl = 'https://login.yandex.ru/info';
|
||||
const response = await fetch(baseUrl, {
|
||||
headers: {
|
||||
Authorization: `OAuth ${token}`
|
||||
}
|
||||
})
|
||||
return (await response.json()).id;
|
||||
}
|
||||
24
backend/src/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import "https://deno.land/x/dotenv@v3.2.0/load.ts";
|
||||
import { Application } from "https://deno.land/x/oak@v10.5.1/mod.ts";
|
||||
import { Session, CookieStore } from "https://deno.land/x/oak_sessions@v3.4.0/mod.ts";
|
||||
import { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";
|
||||
|
||||
import routes from './routes.ts'
|
||||
|
||||
const PORT = Number(Deno.env.get('PORT'));
|
||||
const SECRET = Deno.env.get('SECRET');
|
||||
|
||||
const app = new Application();
|
||||
const store = new CookieStore(SECRET);
|
||||
const session = new Session(store);
|
||||
|
||||
app.use(session.initMiddleware());
|
||||
app.use(oakCors({
|
||||
origin: 'http://localhost:8080',
|
||||
credentials: true
|
||||
}));
|
||||
app.use(routes.routes());
|
||||
app.use(routes.allowedMethods());
|
||||
|
||||
console.log(`👂 on port ${PORT}`);
|
||||
await app.listen({ port: PORT });
|
||||
9
backend/src/models/Error.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export default class Error {
|
||||
msg: string;
|
||||
code: string;
|
||||
|
||||
constructor(msg: string, type: string, code: number) {
|
||||
this.msg = msg;
|
||||
this.code = `${type}-${code}`;
|
||||
}
|
||||
}
|
||||
31
backend/src/models/Session.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export default class Session {
|
||||
userId: string;
|
||||
token: string;
|
||||
expireAt: Date;
|
||||
lifetime = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
constructor({
|
||||
userId,
|
||||
token,
|
||||
expireAt
|
||||
}: {
|
||||
userId: string,
|
||||
token?: string,
|
||||
expireAt?: Date
|
||||
}) {
|
||||
const nowDate = new Date();
|
||||
|
||||
this.userId = userId;
|
||||
this.token = token ?? crypto.randomUUID();
|
||||
this.expireAt = expireAt ?? new Date(nowDate.getTime() + this.lifetime);
|
||||
}
|
||||
|
||||
extend() {
|
||||
const nowDate = new Date();
|
||||
this.expireAt = new Date(nowDate.getTime() + this.lifetime);
|
||||
}
|
||||
|
||||
revoke() {
|
||||
this.expireAt = new Date();
|
||||
}
|
||||
}
|
||||
21
backend/src/models/Task.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export default class Task {
|
||||
name: string;
|
||||
startedAt?: Date;
|
||||
running: boolean;
|
||||
|
||||
constructor({
|
||||
name,
|
||||
startedAt,
|
||||
totalTime,
|
||||
category
|
||||
}: {
|
||||
name: string,
|
||||
startedAt?: Date,
|
||||
totalTime: number,
|
||||
category: string | null
|
||||
}) {
|
||||
this.name = name;
|
||||
this.startedAt = startedAt;
|
||||
this.running = false
|
||||
}
|
||||
}
|
||||
45
backend/src/models/User.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import Task from './Task.ts';
|
||||
|
||||
export default class User {
|
||||
id: string;
|
||||
providerId: string;
|
||||
authProvider: string;
|
||||
tasks: Array<Task>;
|
||||
categories: Array<string>;
|
||||
updatedAt: Date;
|
||||
|
||||
constructor({
|
||||
providerId,
|
||||
authProvider,
|
||||
tasks,
|
||||
categories,
|
||||
updatedAt
|
||||
}: {
|
||||
providerId: string,
|
||||
authProvider: string,
|
||||
tasks?: Array<Task>,
|
||||
categories?: Array<string>,
|
||||
updatedAt?: Date | string
|
||||
}) {
|
||||
this.providerId = providerId;
|
||||
this.authProvider = authProvider;
|
||||
this.id = `${providerId}@${authProvider}`;
|
||||
this.tasks = tasks ?? [];
|
||||
this.categories = categories ?? [];
|
||||
if (updatedAt instanceof Date) {
|
||||
this.updatedAt = updatedAt;
|
||||
} else if (updatedAt) {
|
||||
this.updatedAt = new Date(updatedAt);
|
||||
} else {
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
addTask(task: Task) {
|
||||
this.tasks.push(task);
|
||||
}
|
||||
|
||||
removeTask(taskName: string) {
|
||||
this.tasks = this.tasks.filter((task) => task.name !== taskName);
|
||||
}
|
||||
}
|
||||
89
backend/src/routes.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import "https://deno.land/x/dotenv@v3.2.0/load.ts";
|
||||
import { Router } from "https://deno.land/x/oak@v10.5.1/mod.ts";
|
||||
import { MongoClient } from "https://deno.land/x/mongo@v0.29.4/mod.ts";
|
||||
|
||||
import User from './models/User.ts';
|
||||
import authorized from './authorized.ts';
|
||||
import getProviderId from './getProviderId.ts';
|
||||
|
||||
const MONGODB_URI = String(Deno.env.get('MONGODB_URI'));
|
||||
|
||||
const client = new MongoClient();
|
||||
await client.connect(MONGODB_URI);
|
||||
|
||||
const db = client.database("worktime");
|
||||
const users = db.collection<User>("users");
|
||||
|
||||
const endpoints = new Router()
|
||||
.post("/login", async (ctx) => {
|
||||
const body = await ctx.request.body({ type: "json" }).value;
|
||||
const userId = await ctx.state.session.get('userId')
|
||||
const token = body.token;
|
||||
const authProvider = body.authProvider;
|
||||
|
||||
if (userId) {
|
||||
ctx.response.body = 'success';
|
||||
return
|
||||
}
|
||||
|
||||
const providerId = await getProviderId(token, authProvider);
|
||||
if (!providerId) {
|
||||
ctx.response.status = 500
|
||||
ctx.response.body = 'error';
|
||||
return
|
||||
}
|
||||
|
||||
let user = await users.findOne({
|
||||
providerId,
|
||||
authProvider
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = new User({
|
||||
providerId,
|
||||
authProvider
|
||||
});
|
||||
await users.insertOne(user);
|
||||
}
|
||||
|
||||
await ctx.state.session.set('userId', user.id);
|
||||
|
||||
ctx.response.body = 'success';
|
||||
})
|
||||
.post('/sync', authorized, async (ctx) => {
|
||||
const body = await ctx.request.body({ type: "json" }).value;
|
||||
const userId = await ctx.state.session.get('userId')
|
||||
const clientSideUser = new User(body.user);
|
||||
|
||||
const serverSideUser = await users.findOne({ id: userId })
|
||||
if (!serverSideUser) {
|
||||
ctx.response.status = 500
|
||||
ctx.response.body = 'error';
|
||||
return
|
||||
}
|
||||
|
||||
if (serverSideUser.updatedAt <= clientSideUser.updatedAt) {
|
||||
serverSideUser.tasks = clientSideUser.tasks;
|
||||
serverSideUser.categories = clientSideUser.categories;
|
||||
serverSideUser.updatedAt = new Date();
|
||||
}
|
||||
|
||||
await users.updateOne({ id: userId }, {
|
||||
$set: {
|
||||
tasks: serverSideUser.tasks,
|
||||
categories: serverSideUser.categories,
|
||||
updatedAt: serverSideUser.updatedAt
|
||||
}
|
||||
})
|
||||
|
||||
ctx.response.body = {
|
||||
tasks: serverSideUser.tasks,
|
||||
categories: serverSideUser.categories,
|
||||
updatedAt: serverSideUser.updatedAt,
|
||||
};
|
||||
})
|
||||
|
||||
const routes = new Router()
|
||||
.use("/api", endpoints.routes(), endpoints.allowedMethods())
|
||||
|
||||
export default routes;
|
||||
2
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
/dist
|
||||
891
package-lock.json → frontend/package-lock.json
generated
@@ -5,6 +5,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"dev": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"predeploy": "npm run build",
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
@@ -4,6 +4,16 @@
|
||||
<div id="modalSpot" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api';
|
||||
|
||||
export default {
|
||||
async mounted() {
|
||||
await api.sync();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/scss/style.scss";
|
||||
|
||||
40
frontend/src/api/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import store from '@/store';
|
||||
|
||||
class Api {
|
||||
baseUrl = 'http://localhost:3000/api';
|
||||
|
||||
async login(token, authProvider) {
|
||||
const response = await fetch(`${this.baseUrl}/login`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
authProvider,
|
||||
}),
|
||||
});
|
||||
console.log(await response.json());
|
||||
}
|
||||
|
||||
async sync() {
|
||||
const { tasks, categories, updatedAt } = store.state;
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/sync`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
user: {
|
||||
tasks,
|
||||
categories,
|
||||
updatedAt,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const syncedData = await response.json();
|
||||
store.commit('setTasks', syncedData.tasks);
|
||||
store.commit('setCategories', syncedData.categories);
|
||||
store.commit('updated');
|
||||
}
|
||||
}
|
||||
|
||||
export default new Api();
|
||||
|
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 332 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 289 B After Width: | Height: | Size: 289 B |
|
Before Width: | Height: | Size: 273 B After Width: | Height: | Size: 273 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 248 B After Width: | Height: | Size: 248 B |
|
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 568 B |
19
frontend/src/assets/yandex_login.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="260" height="56" viewBox="0 0 260 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="260" height="56" fill="black"/>
|
||||
<rect x="44" y="16" width="24" height="24" rx="12" fill="#FC3F1D"/>
|
||||
<path d="M57.6912 35.212H60.1982V20.812H56.5516C52.8843 20.812 50.9574 22.6975 50.9574 25.4739C50.9574 27.6909 52.0141 28.9962 53.8995 30.3429L50.6259 35.212H53.3401L56.9867 29.7628L55.7228 28.9133C54.1896 27.8773 53.4437 27.0693 53.4437 25.3288C53.4437 23.7956 54.5211 22.7596 56.5723 22.7596H57.6912V35.212Z" fill="white"/>
|
||||
<path d="M81.3961 21.528H85.3001C86.6655 21.528 87.7055 21.7413 88.4201 22.168C89.1348 22.5947 89.4921 23.3147 89.4921 24.328C89.4921 24.744 89.4335 25.1067 89.3161 25.416C89.1988 25.7147 89.0281 25.976 88.8041 26.2C88.5908 26.4133 88.3295 26.5893 88.0201 26.728C87.7108 26.8667 87.3695 26.9733 86.9961 27.048C87.9561 27.1653 88.6815 27.4267 89.1721 27.832C89.6628 28.2373 89.9081 28.856 89.9081 29.688C89.9081 30.2853 89.7908 30.7973 89.5561 31.224C89.3215 31.64 88.9961 31.9813 88.5801 32.248C88.1641 32.504 87.6735 32.696 87.1081 32.824C86.5428 32.9413 85.9295 33 85.2681 33H81.3961V21.528ZM83.3321 23.208V26.264H85.3961C86.0361 26.264 86.5535 26.1307 86.9481 25.864C87.3428 25.5867 87.5401 25.1547 87.5401 24.568C87.5401 24.0347 87.3588 23.6773 86.9961 23.496C86.6441 23.304 86.1428 23.208 85.4921 23.208H83.3321ZM83.3321 27.928V31.336H85.4601C85.8228 31.336 86.1535 31.3093 86.4521 31.256C86.7508 31.192 87.0068 31.096 87.2201 30.968C87.4335 30.8293 87.5988 30.6533 87.7161 30.44C87.8335 30.216 87.8921 29.944 87.8921 29.624C87.8921 28.9947 87.6841 28.5573 87.2681 28.312C86.8628 28.056 86.2068 27.928 85.3001 27.928H83.3321Z" fill="white"/>
|
||||
<path d="M95.2729 33.16C94.6862 33.16 94.1422 33.064 93.6409 32.872C93.1395 32.68 92.7022 32.4027 92.3289 32.04C91.9662 31.6667 91.6782 31.2133 91.4649 30.68C91.2622 30.1467 91.1609 29.5333 91.1609 28.84C91.1609 28.1467 91.2622 27.5333 91.4649 27C91.6782 26.4667 91.9662 26.0187 92.3289 25.656C92.7022 25.2827 93.1395 25.0053 93.6409 24.824C94.1422 24.632 94.6862 24.536 95.2729 24.536C95.8595 24.536 96.4035 24.632 96.9049 24.824C97.4062 25.0053 97.8435 25.2827 98.2169 25.656C98.5902 26.0187 98.8835 26.4667 99.0969 27C99.3102 27.5333 99.4169 28.1467 99.4169 28.84C99.4169 29.5333 99.3102 30.1467 99.0969 30.68C98.8835 31.2133 98.5902 31.6667 98.2169 32.04C97.8435 32.4027 97.4062 32.68 96.9049 32.872C96.4035 33.064 95.8595 33.16 95.2729 33.16ZM95.2729 31.608C95.9129 31.608 96.4409 31.384 96.8569 30.936C97.2835 30.488 97.4969 29.7893 97.4969 28.84C97.4969 27.9013 97.2835 27.208 96.8569 26.76C96.4409 26.3013 95.9129 26.072 95.2729 26.072C94.6435 26.072 94.1155 26.3013 93.6889 26.76C93.2729 27.208 93.0649 27.9013 93.0649 28.84C93.0649 29.7893 93.2729 30.488 93.6889 30.936C94.1155 31.384 94.6435 31.608 95.2729 31.608Z" fill="white"/>
|
||||
<path d="M104.714 23.704C104.266 23.704 103.876 23.6453 103.546 23.528C103.226 23.4107 102.954 23.2507 102.73 23.048C102.516 22.8347 102.356 22.5893 102.25 22.312C102.143 22.024 102.09 21.72 102.09 21.4H103.722C103.722 21.7733 103.807 22.0453 103.978 22.216C104.159 22.376 104.404 22.456 104.714 22.456C105.023 22.456 105.263 22.376 105.434 22.216C105.604 22.0453 105.69 21.7733 105.69 21.4H107.338C107.338 21.72 107.284 22.024 107.178 22.312C107.071 22.5893 106.906 22.8347 106.682 23.048C106.468 23.2507 106.196 23.4107 105.866 23.528C105.535 23.6453 105.151 23.704 104.714 23.704ZM102.81 30.488L106.65 24.696H108.474V33H106.634V27.24L102.826 33H100.97V24.696H102.81V30.488Z" fill="white"/>
|
||||
<path d="M114.275 26.2V33H112.419V26.2H109.907V24.696H116.835V26.2H114.275Z" fill="white"/>
|
||||
<path d="M120.091 30.488L123.931 24.696H125.755V33H123.915V27.24L120.107 33H118.251V24.696H120.091V30.488Z" fill="white"/>
|
||||
<path d="M135.184 33.16C134.512 33.16 133.91 33.064 133.376 32.872C132.843 32.6693 132.39 32.3867 132.016 32.024C131.643 31.6507 131.355 31.1973 131.152 30.664C130.95 30.1307 130.848 29.5227 130.848 28.84C130.848 28.168 130.95 27.5653 131.152 27.032C131.355 26.4987 131.643 26.0507 132.016 25.688C132.39 25.3147 132.848 25.032 133.392 24.84C133.936 24.6373 134.544 24.536 135.216 24.536C135.792 24.536 136.294 24.6 136.72 24.728C137.158 24.856 137.504 25.016 137.76 25.208V26.744C137.43 26.5307 137.067 26.3653 136.672 26.248C136.288 26.1307 135.84 26.072 135.328 26.072C133.611 26.072 132.752 26.9947 132.752 28.84C132.752 30.6853 133.595 31.608 135.28 31.608C135.824 31.608 136.288 31.5493 136.672 31.432C137.067 31.304 137.43 31.144 137.76 30.952V32.488C137.483 32.6693 137.136 32.8293 136.72 32.968C136.304 33.096 135.792 33.16 135.184 33.16Z" fill="white"/>
|
||||
<path d="M143.097 25.128C143.097 24.52 143.204 23.992 143.417 23.544C143.631 23.0853 143.929 22.712 144.313 22.424C144.697 22.1253 145.156 21.9013 145.689 21.752C146.233 21.6027 146.836 21.528 147.497 21.528H151.225V33H149.273V28.584H147.497L144.569 33H142.313L145.545 28.296C144.713 28.0933 144.095 27.7253 143.689 27.192C143.295 26.648 143.097 25.96 143.097 25.128ZM149.273 26.968V23.208H147.481C146.756 23.208 146.175 23.352 145.737 23.64C145.311 23.9173 145.097 24.3973 145.097 25.08C145.097 25.752 145.289 26.2373 145.673 26.536C146.057 26.824 146.601 26.968 147.305 26.968H149.273Z" fill="white"/>
|
||||
<path d="M158.845 29.496H155.341V33H153.485V24.696H155.341V27.992H158.845V24.696H160.701V33H158.845V29.496Z" fill="white"/>
|
||||
<path d="M162.558 31.496C162.804 31.2827 162.996 30.9893 163.134 30.616C163.284 30.2427 163.401 29.7787 163.486 29.224C163.572 28.6587 163.636 28.0027 163.678 27.256C163.721 26.5093 163.758 25.656 163.79 24.696H169.87V31.496H171.118V35.368H169.502L169.358 33H163.63L163.486 35.368H161.854V31.496H162.558ZM168.014 31.496V26.2H165.39C165.337 27.512 165.246 28.6 165.118 29.464C165.001 30.3173 164.804 30.9947 164.526 31.496H168.014Z" fill="white"/>
|
||||
<path d="M179.273 32.392C179.156 32.4667 179.012 32.552 178.841 32.648C178.67 32.7333 178.468 32.8133 178.233 32.888C177.998 32.9627 177.726 33.0267 177.417 33.08C177.108 33.1333 176.756 33.16 176.361 33.16C174.836 33.16 173.694 32.7813 172.937 32.024C172.19 31.2667 171.817 30.2053 171.817 28.84C171.817 28.168 171.918 27.5653 172.121 27.032C172.324 26.4987 172.606 26.0507 172.969 25.688C173.332 25.3147 173.764 25.032 174.265 24.84C174.766 24.6373 175.316 24.536 175.913 24.536C176.532 24.536 177.086 24.6373 177.577 24.84C178.078 25.0427 178.489 25.3467 178.809 25.752C179.129 26.1573 179.348 26.6587 179.465 27.256C179.593 27.8533 179.598 28.552 179.481 29.352H173.737C173.812 30.1093 174.062 30.68 174.489 31.064C174.916 31.4373 175.582 31.624 176.489 31.624C177.15 31.624 177.7 31.544 178.137 31.384C178.585 31.2133 178.964 31.0373 179.273 30.856V32.392ZM175.913 26.008C175.369 26.008 174.91 26.1733 174.537 26.504C174.164 26.8347 173.918 27.3253 173.801 27.976H177.705C177.726 27.304 177.577 26.808 177.257 26.488C176.937 26.168 176.489 26.008 175.913 26.008Z" fill="white"/>
|
||||
<path d="M183.78 29.624H183.06V33H181.204V24.696H183.06V28.12H183.844L186.484 24.696H188.436L185.3 28.696L188.564 33H186.372L183.78 29.624Z" fill="white"/>
|
||||
<path d="M193.231 33.16C192.559 33.16 191.957 33.064 191.423 32.872C190.89 32.6693 190.437 32.3867 190.063 32.024C189.69 31.6507 189.402 31.1973 189.199 30.664C188.997 30.1307 188.895 29.5227 188.895 28.84C188.895 28.168 188.997 27.5653 189.199 27.032C189.402 26.4987 189.69 26.0507 190.063 25.688C190.437 25.3147 190.895 25.032 191.439 24.84C191.983 24.6373 192.591 24.536 193.263 24.536C193.839 24.536 194.341 24.6 194.767 24.728C195.205 24.856 195.551 25.016 195.807 25.208V26.744C195.477 26.5307 195.114 26.3653 194.719 26.248C194.335 26.1307 193.887 26.072 193.375 26.072C191.658 26.072 190.799 26.9947 190.799 28.84C190.799 30.6853 191.642 31.608 193.327 31.608C193.871 31.608 194.335 31.5493 194.719 31.432C195.114 31.304 195.477 31.144 195.807 30.952V32.488C195.53 32.6693 195.183 32.8293 194.767 32.968C194.351 33.096 193.839 33.16 193.231 33.16Z" fill="white"/>
|
||||
<path d="M203.16 21.528V33H201.224V21.528H203.16Z" fill="white"/>
|
||||
<path d="M205.615 21.528H209.487C210.298 21.528 211.05 21.6187 211.743 21.8C212.447 21.9813 213.055 22.28 213.567 22.696C214.09 23.112 214.495 23.6613 214.783 24.344C215.082 25.0267 215.231 25.8693 215.231 26.872C215.231 27.8853 215.082 28.776 214.783 29.544C214.495 30.3013 214.084 30.936 213.551 31.448C213.028 31.96 212.404 32.3493 211.679 32.616C210.964 32.872 210.18 33 209.327 33H205.615V21.528ZM207.551 23.208V31.336H209.391C209.956 31.336 210.468 31.2507 210.927 31.08C211.396 30.9093 211.796 30.648 212.127 30.296C212.468 29.944 212.73 29.496 212.911 28.952C213.103 28.3973 213.199 27.7413 213.199 26.984C213.199 26.2373 213.108 25.624 212.927 25.144C212.746 24.6533 212.49 24.264 212.159 23.976C211.839 23.688 211.455 23.4907 211.007 23.384C210.559 23.2667 210.074 23.208 209.551 23.208H207.551Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
38
frontend/src/components/Auth.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="auth">
|
||||
<img
|
||||
class="button"
|
||||
src="@/assets/yandex_login.svg"
|
||||
@click="yandexLogin"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
yandexLogin() {
|
||||
const baseUrl = 'https://oauth.yandex.ru/authorize';
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'token',
|
||||
client_id: '081b0b290f0847e18b19771c840d1075',
|
||||
redirect_uri: 'http://localhost:8080/auth-redirect',
|
||||
state: 'yandex',
|
||||
});
|
||||
window.location = `${baseUrl}?${params.toString()}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.auth {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@
|
||||
<input type="checkbox" id="reset" v-model="resetAtMidnight">
|
||||
<label for="reset">Reset all tasks to 0 at midnight</label>
|
||||
</div>
|
||||
<Auth />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -12,10 +13,12 @@
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import Auth from './Auth.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
Auth,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
23
frontend/src/router/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import Home from '../views/Home/Index.vue';
|
||||
import AuthRedirect from '../views/AuthRedirect.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: '/auth-redirect',
|
||||
name: 'AuthRedirect',
|
||||
component: AuthRedirect,
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -11,15 +11,18 @@ export default createStore({
|
||||
tasks: [],
|
||||
midnightReset: false,
|
||||
lastReset: new Date(),
|
||||
updatedAt: new Date(),
|
||||
darkTheme: true,
|
||||
},
|
||||
mutations: {
|
||||
addCategory(state, category) {
|
||||
this.commit('updated');
|
||||
if (category && !state.categories.includes(category)) {
|
||||
state.categories = [...state.categories, category];
|
||||
}
|
||||
},
|
||||
removeCategory(state, category) {
|
||||
this.commit('updated');
|
||||
state.categories = state.categories.filter((element) => element !== category);
|
||||
state.tasks = state.tasks.map((task) => {
|
||||
const newTask = task;
|
||||
@@ -28,6 +31,7 @@ export default createStore({
|
||||
});
|
||||
},
|
||||
assignCategory(state, { name, category }) {
|
||||
this.commit('updated');
|
||||
state.tasks = state.tasks.map((task) => {
|
||||
const newTask = task;
|
||||
if (newTask.name === name) {
|
||||
@@ -36,8 +40,12 @@ export default createStore({
|
||||
return newTask;
|
||||
});
|
||||
},
|
||||
setCategories(state, categories) {
|
||||
state.categories = categories;
|
||||
},
|
||||
|
||||
addTask(state, name) {
|
||||
this.commit('updated');
|
||||
if (name) {
|
||||
const task = {
|
||||
name,
|
||||
@@ -50,12 +58,15 @@ export default createStore({
|
||||
}
|
||||
},
|
||||
removeTask(state, name) {
|
||||
this.commit('updated');
|
||||
state.tasks = state.tasks.filter((task) => task.name !== name);
|
||||
},
|
||||
removeAllTasks(state) {
|
||||
this.commit('updated');
|
||||
state.tasks = [];
|
||||
},
|
||||
startTask(state, name) {
|
||||
this.commit('updated');
|
||||
state.tasks = state.tasks.map((task) => {
|
||||
const newTask = task;
|
||||
if (newTask.name === name) {
|
||||
@@ -70,6 +81,7 @@ export default createStore({
|
||||
});
|
||||
},
|
||||
stopTask(state, name) {
|
||||
this.commit('updated');
|
||||
state.tasks = state.tasks.map((task) => {
|
||||
const newTask = task;
|
||||
if (newTask.name === name) {
|
||||
@@ -81,6 +93,7 @@ export default createStore({
|
||||
});
|
||||
},
|
||||
resetTasks(state) {
|
||||
this.commit('updated');
|
||||
state.tasks = state.tasks.map((task) => {
|
||||
const newTask = task;
|
||||
newTask.running = false;
|
||||
@@ -90,10 +103,17 @@ export default createStore({
|
||||
});
|
||||
state.lastReset = new Date();
|
||||
},
|
||||
setTasks(state, tasks) {
|
||||
state.tasks = tasks;
|
||||
},
|
||||
|
||||
setMidnightReset(state, value) {
|
||||
state.midnightReset = !!value;
|
||||
},
|
||||
|
||||
updated(state, at) {
|
||||
state.updatedAt = at ?? new Date();
|
||||
},
|
||||
},
|
||||
plugins: [vuexLocal.plugin],
|
||||
});
|
||||
18
frontend/src/views/AuthRedirect.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
You should be redirected in a moment.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api';
|
||||
|
||||
export default {
|
||||
async mounted() {
|
||||
const token = /access_token=([^&]+)/.exec(this.$route.hash)[1];
|
||||
const authProvider = /state=([^&]+)/.exec(this.$route.hash)[1];
|
||||
|
||||
api.login(token, authProvider);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,7 +1,4 @@
|
||||
module.exports = {
|
||||
publicPath: process.env.NODE_ENV === 'production'
|
||||
? '/worktime/'
|
||||
: '/',
|
||||
pwa: {
|
||||
appleMobileWebAppCapable: 'yes',
|
||||
appleMobileWebAppStatusBarStyle: 'black-transculent',
|
||||
@@ -1,17 +0,0 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import Home from '../views/Home/Index.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||