Compare commits

...

5 Commits

19 changed files with 1184 additions and 85 deletions

695
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@
"format": "npx prettier --write ."
},
"dependencies": {
"bcrypt": "^5.1.1",
"colors": "1.4.0",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
@@ -33,6 +34,7 @@
"sequelize": "^6.15.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.10",
"@types/ejs": "^3.0.6",
@@ -46,6 +48,7 @@
"@types/shelljs": "^0.8.9",
"jest": "^27.0.6",
"morgan": "^1.10.0",
"nodemon": "^3.0.2",
"npm-run-all": "^4.1.5",
"rimraf": "^3.0.2",
"shelljs": "^0.8.4",

View File

@@ -1,13 +1,21 @@
import env from './environment'
import { Err, Succ } from '../services/globalService'
import db from './sequelize.config'
import Client from '../models/Client'
import Project from '../models/Project'
import Task from '../models/Task'
import User from '../models/User'
function connect() {
async function connect() {
if (!env.NORK.database) {
new Err(500, 'no database is in norkcfg.json')
return false
}
if (env.NORK.database.orm == 'sequelize') {
await User.sync()
await Client.sync()
await Project.sync()
await Task.sync({ alter: true })
db.sync()
.then(() => {
new Succ(200, 'connected to db')

View File

@@ -1,5 +1,6 @@
import { Request, Response } from 'express'
import Task from '../models/Task'
import { Err, Succ } from '../services/globalService'
export async function get(req: Request, res: Response) {
try {
@@ -11,9 +12,34 @@ export async function get(req: Request, res: Response) {
}
export async function add(req: Request, res: Response) {
try {
const task = await Task.create(req.body)
const payload = req.body
payload.author_id = res.locals.user._id
console.log(payload)
const task = await Task.create(payload)
res.json(task)
} catch (error) {
res.status(500).send(error)
}
}
export async function edit(req: Request, res: Response) {
try {
const payload = req.body
payload.author_id = res.locals.user._id
const task: any = await Task.findByPk(payload._id)
if (!task) {
return res.status(400).json(new Err(400, 'Task not found'))
}
task.title = payload.title
task.timeEnd = payload.timeEnd
task.timeStart = payload.timeStart
await task.save()
return res.json(new Succ(200, 'Task updated succesfully'))
} catch (error) {
return res.status(500).json(new Err(400, 'something went wrong', error))
}
}

View File

@@ -0,0 +1,76 @@
import { Request, Response } from 'express'
import bcrypt from 'bcrypt'
import jwt from 'jsonwebtoken'
import env from '../config/environment'
import User from '../models/User'
import { Err, Succ } from '../services/globalService'
export async function login(req: Request, res: Response) {}
export async function signup(req: Request, res: Response) {
try {
const payload = req.body
payload.password = await bcrypt.hash(payload.password, 12)
const user = await User.create(payload)
res.status(201).json(new Succ(201, 'user was successfully signed up'))
} catch (err: any) {
new Err(500, err)
res.status(500).json(new Err(500, 'something went wrong'))
}
}
export async function signin(req: Request, res: Response) {
try {
const payload = req.body
const user: any = await User.findOne({ where: { email: payload.email } })
if (!user) {
res.cookie('jwt', '', { httpOnly: true, maxAge: 0 })
res.cookie('auth', false, { httpOnly: false, maxAge: 0 })
res.status(401).json(new Err(401, 'email or password is wrong'))
return
}
if (await bcrypt.compare(payload.password, user.password)) {
const maxAge = 3 * 24 * 60 * 60 // 3 days in seconds
const createToken = (id: any) => {
return jwt.sign({ id }, env.JWT_SECRET, {
expiresIn: maxAge
})
}
const token = createToken(user._id)
res.cookie('jwt', token, { httpOnly: true, maxAge: maxAge * 1000 })
res.cookie('auth', true, { httpOnly: false, maxAge: maxAge * 1000 })
res.json(new Succ(200, 'user is logged in'))
return
}
res.cookie('jwt', '', { httpOnly: true, maxAge: 0 })
res.cookie('auth', false, { httpOnly: false, maxAge: 0 })
res.status(401).json(new Err(401, 'email or password is wrong'))
} catch (err: any) {
new Err(500, err)
res.status(500).json(new Err(500, 'something went wrong'))
}
}
export function logout(req: Request, res: Response) {
res.cookie('jwt', '', { httpOnly: true, maxAge: 0 })
res.cookie('auth', false, { httpOnly: false, maxAge: 0 })
res.json(new Succ(200, 'user was logged out'))
}
export function status(req: Request, res: Response) {
try {
let userObject = res.locals.user
userObject.password = undefined
userObject.__v = undefined
res.status(200).json(new Succ(200, 'user is logged in', userObject))
} catch (error) {
res.status(500).json(new Err(500, 'somehting went wrong', error))
}
}

View File

@@ -6,7 +6,7 @@ import User from '../models/User' // uncomment this
export function requireAuth(req: Request, res: Response, next: NextFunction) {
const token = req.cookies.jwt
new Err(500, 'uncomment code in authMiddleware before using!')
//new Err(500, 'uncomment code in authMiddleware before using!')
if (token) {
jwt.verify(token, env.JWT_SECRET, async (err: any, decodedToken: any) => {
if (err) {
@@ -14,25 +14,16 @@ export function requireAuth(req: Request, res: Response, next: NextFunction) {
res.status(401).json(new Err(401, 'user is not authenticated'))
}
if (!err) {
const user = (async () => {
if (env.NORK.db.orm) {
if (env.NORK.db.orm == 'sequelize') {
return await User.findByPk(decodedToken.id)
}
if (env.NORK.db.orm == 'mongoose') {
return await User.findById(decodedToken.id)
}
} else {
return null
}
})()
const user = await User.findByPk(decodedToken.id)
console.log('TADY', user)
if (user === null) {
console.log('1')
res.status(401).json(new Err(401, 'user is not authenticated'))
return
}
res.locals.user = user
console.log('2')
new Succ(100, 'user is authenticated')
next()
}
@@ -40,6 +31,7 @@ export function requireAuth(req: Request, res: Response, next: NextFunction) {
}
if (!token) {
console.log('3')
res.status(401).json(new Err(401, 'user is not authenticated'))
}
}

44
api/src/models/Client.ts Normal file
View File

@@ -0,0 +1,44 @@
import { DataTypes, Model } from 'sequelize'
import path from 'path'
import db from '../config/sequelize.config'
import User from './User'
class Instance extends Model {}
Instance.init(
{
_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false,
unique: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
hourlyRate: {
type: DataTypes.INTEGER,
allowNull: true
},
contact: {
type: DataTypes.STRING,
allowNull: true
},
author_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: User,
key: '_id'
}
}
},
{
sequelize: db,
tableName: path.basename(__filename).split('.')[0].toLowerCase()
}
)
export default Instance

49
api/src/models/Project.ts Normal file
View File

@@ -0,0 +1,49 @@
import { DataTypes, Model } from 'sequelize'
import path from 'path'
import db from '../config/sequelize.config'
import User from './User'
import Client from './Client'
class Instance extends Model {}
Instance.init(
{
_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false,
unique: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
hourlyRate: {
type: DataTypes.INTEGER,
allowNull: true
},
client_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: Client,
key: '_id'
}
},
author_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: User,
key: '_id'
}
}
},
{
sequelize: db,
tableName: path.basename(__filename).split('.')[0].toLowerCase()
}
)
export default Instance

View File

@@ -1,6 +1,9 @@
import { DataTypes, Model } from 'sequelize'
import path from 'path'
import db from '../config/sequelize.config'
import User from './User'
import Client from './Client'
import Project from './Project'
class Instance extends Model {}
@@ -14,14 +17,6 @@ Instance.init(
unique: true
},
title: {
type: DataTypes.STRING,
allowNull: false
},
client: {
type: DataTypes.STRING,
allowNull: true
},
project: {
type: DataTypes.STRING,
allowNull: true
},
@@ -32,6 +27,22 @@ Instance.init(
timeEnd: {
type: DataTypes.BIGINT,
allowNull: true
},
project_id: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: Project,
key: '_id'
}
},
author_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: User,
key: '_id'
}
}
},
{

View File

@@ -8,28 +8,29 @@ Instance.init(
{
_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false,
unique: true,
unique: true
},
username: {
type: DataTypes.STRING,
allowNull: false,
allowNull: false
},
password: {
type: DataTypes.STRING,
allowNull: false,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
unique: true
}
},
{
sequelize: db,
tableName: path.basename(__filename).split('.')[0].toLowerCase(),
},
tableName: path.basename(__filename).split('.')[0].toLowerCase()
}
)
export default Instance

View File

@@ -1,8 +1,16 @@
import { Router } from 'express'
import * as taskController from "../controllers/taskController"
import * as taskController from '../controllers/taskController'
import * as userController from '../controllers/userController'
import { requireAuth } from '../middlewares/authMiddleware'
export const router = Router()
//const mws = [handleValidation.handleValidationError]
router.get("/task/get", taskController.get)
router.post("/task/add", taskController.add)
router.get('/task/get', requireAuth, taskController.get)
router.post('/task/add', requireAuth, taskController.add)
router.post('/task/edit', requireAuth, taskController.edit)
router.post('/auth/signup', userController.signup)
router.post('/auth/signin', userController.signin)
router.post('/auth/logout', requireAuth, userController.logout)
router.get('/auth/status', requireAuth, userController.status)

47
frontend/src/api/user.js Normal file
View File

@@ -0,0 +1,47 @@
const api_url = 'http://localhost:6060'
const userAPIS = {
async getUserStatus() {
const res = await fetch(`${api_url}/api/v1/auth/status`, {
credentials: 'include'
})
return res.json()
},
async loginUser(formState) {
const res = await fetch(`${api_url}/api/v1/auth/signin`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formState)
})
return await res.json()
},
async logoutUser() {
const res = await fetch(`${api_url}/api/v1/auth/logout`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
})
return await res.json()
},
async registerUser(formState) {
const res = await fetch(`${api_url}/api/v1/auth/signup`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formState)
})
return res.json()
}
}
export default userAPIS

View File

@@ -0,0 +1,37 @@
<script setup>
defineProps(['btn-txt', 'api-endpoint'])
</script>
<template>
<form>
<slot></slot>
<button @click="formAction">{{ btnTxt }}</button>
</form>
</template>
<script>
import AppStore from '@/stores/AppStore'
export default {
data() {
return {
d: {
email: undefined,
password: undefined,
username: undefined
}
}
},
methods: {
formAction() {
console.log(this.d)
//AppStore.sendAdd(null, this.$props.apiEndpoint)
}
}
}
</script>
<style scoped>
form {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View File

@@ -30,37 +30,78 @@ export default {
timer: undefined,
timerState: 'Start',
task: {
startTime: 0,
stopTime: 0,
timeStart: 0,
timeEnd: 0,
title: ''
}
},
restore: false
}
},
computed: {
formattedElapsedTime() {
const date = new Date(this.timeNow - this.task.startTime)
const date = new Date(this.timeNow - this.task.timeStart)
const utc = date.toUTCString()
return utc.substr(utc.indexOf(':') - 2, 8)
}
},
methods: {
startStopTimer() {
async startStopTimer() {
if (this.timerState == 'Start') {
this.timerState = 'Stop'
this.task.startTime = Date.now()
console.log('TADY', this.restore)
if (this.restore == false) {
console.log('TADY')
this.task.timeStart = Date.now()
}
this.timeNow = Date.now()
console.log('timer started')
this.timer = setInterval(() => {
this.timeNow = Date.now()
}, 1000)
if (!this.task._id) {
AppStore.data.newTask = {
title: this.task.title,
timeStart: this.task.timeStart
}
await AppStore.sendAdd(AppStore.data.newTask, '/task/add')
}
} else {
this.task.timeEnd = Date.now()
this.timerState = 'Start'
this.task.stopTime = Date.now()
clearInterval(this.timer)
this.timer = undefined
AppStore.data.newTask = this.task
console.log(AppStore.data.newTask)
this.timeNow = 0
AppStore.data.newTask = {
title: this.task.title,
timeStart: this.task.timeStart,
timeEnd: this.task.timeEnd
}
if (this.task._id) {
AppStore.data.newTask._id = this.task._id
await AppStore.sendAdd(AppStore.data.newTask, '/task/edit')
} else {
await AppStore.sendAdd(AppStore.data.newTask, '/task/add')
}
this.task = {
timeStart: 0,
timeEnd: 0,
title: ''
}
this.restore = false
this.$emit('get-tasks')
}
}
},
async mounted() {
await AppStore.fetchData()
const task = AppStore.data.fetchedTasks.filter((task) => task.timeEnd === null)
if (task.length > 0) {
this.task = task[0]
this.restore = true
this.startStopTimer()
}
}
}
</script>

View File

@@ -2,6 +2,14 @@ import { createRouter, createWebHistory } from 'vue-router'
import TrackerView from '../views/TrackerView.vue'
import SignupView from '../views/SignupView.vue'
import LoginView from '../views/LoginView.vue'
import userAPIS from '../api/user'
const authGuard = async () => {
const data = await userAPIS.getUserStatus()
if (!data.data) {
throw router.push('/')
}
}
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -10,7 +18,7 @@ const router = createRouter({
path: '/',
name: 'home',
//component: HomeView
redirect: '/tracker'
redirect: '/tracker'
},
{
path: '/login',
@@ -26,7 +34,10 @@ const router = createRouter({
path: '/tracker',
name: 'tracker',
component: TrackerView,
alias: '/'
alias: '/',
meta: {
requiresAuth: true
}
},
{
path: '/about',
@@ -39,4 +50,20 @@ const router = createRouter({
]
})
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some((x) => x.meta.requiresAuth)
const requiresGuest = to.matched.some((x) => x.meta.requiresGuest)
if (requiresAuth) {
const userStatus = await userAPIS.getUserStatus()
if (!userStatus.data) {
router.replace('/login')
next()
return
}
}
next()
})
export default router

View File

@@ -20,7 +20,10 @@ export default {
},
async fetchData() {
try {
const response = await fetch(this.api_url + '/task/get')
const response = await fetch(this.api_url + '/task/get', {
credentials: 'include'
})
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
@@ -31,5 +34,22 @@ export default {
} catch (error) {
console.error('Error fetching data:', error)
}
},
async sendAdd(data, url) {
try {
const response = await fetch(this.api_url + url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
} catch (error) {
console.error('Error sending data:', error)
}
}
}

View File

@@ -1,15 +1,33 @@
<script setup>
import router from '@/router'
</script>
<template>
<form>
<input type="email" placeholder="Email">
<input type="password" placeholder="Password">
<button>Log in</button>
</form>
<form>
<label for="email">Email:</label>
<input type="email" id="email" v-model="email" placeholder="Email" />
<label for="password">Password:</label>
<input type="password" id="password" v-model="password" placeholder="Password" />
<button @click.prevent="formAction">Log in</button>
</form>
</template>
<style>
form {
display: flex;
flex-direction: column;
align-items: center;
<script>
import User from '@/api/user'
export default {
data() {
return {
email: '',
password: ''
}
</style>
},
methods: {
async formAction() {
const status = await User.loginUser({ email: this.email, password: this.password })
if (status.code == 200) {
throw router.push('/')
}
}
}
}
</script>

View File

@@ -1,3 +1,36 @@
<script setup>
import router from '@/router'
</script>
<template>
<b>signup</b>
</template>
<form>
<label for="username">Username:</label>
<input type="text" id="username" v-model="username" placeholder="Username" />
<label for="email">Email:</label>
<input type="email" id="email" v-model="email" placeholder="Email" />
<label for="password">Password:</label>
<input type="password" id="password" v-model="password" placeholder="Password" />
<button @click.prevent="formAction">Sign up</button>
</form>
</template>
<script>
import User from '@/api/user'
export default {
data() {
return {
username: '',
email: '',
password: ''
}
},
methods: {
async formAction() {
const status = await User.registerUser({ username: this.username, email: this.email, password: this.password })
if (status.code == 201) {
throw router.push('/')
}
}
}
}
</script>

View File

@@ -6,11 +6,11 @@ import TaskRecord from '@/components/TaskRecord.vue'
<template>
<main>
<TrackerTimer />
<TrackerTimer @get-tasks="getTasks" />
<div class="task-day-wrapper">
<TaskHeader />
<div class="task-wrapper" v-for="task in tasks" :key="task._id">
<div class="task-wrapper" v-for="task in sortedData" :key="task._id">
<TaskRecord :task="task" />
</div>
</div>
@@ -26,6 +26,17 @@ export default {
tasks: AppStore.data.fetchedTasks
}
},
methods: {
async getTasks() {
await AppStore.fetchData()
this.tasks = AppStore.data.fetchedTasks
}
},
computed: {
sortedData() {
return this.tasks.slice().sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
}
},
async mounted() {
await AppStore.fetchData()
this.tasks = AppStore.data.fetchedTasks