52 Commits

Author SHA1 Message Date
82a24efd91 Test commit
Some checks failed
Build DeguApp backend / build (pull_request) Failing after 2m15s
2024-06-03 13:22:54 +02:00
5f29773b4a Added: gitea action for backend 2024-06-03 13:22:54 +02:00
7eb548e138 Added: gitea action for backend 2024-06-03 13:22:25 +02:00
6c2ebe7d7a Added: dist/ to biome ignore 2024-06-02 23:06:58 +02:00
1dd7952976 Code formatted 2024-06-02 23:06:32 +02:00
922a11b23b Added: package.json format script 2024-06-02 23:05:57 +02:00
5a2a2db5e2 Fixes: error handling on signup page #13 2024-06-02 23:04:42 +02:00
61449caef1 Updated README.md 2024-06-02 22:34:07 +02:00
c9b8246218 Fix: route for frontend 2024-05-16 15:41:24 +02:00
c89dfa6786 Added: script for building website version 2024-05-15 03:04:16 +02:00
8f3e442077 Added: beer image and other info about beer into reviews 2024-05-15 02:52:01 +02:00
0430710522 Fixed: reviews data now contains info about beer 2024-05-15 02:33:38 +02:00
c1805643c8 Added: print results from API on /reviews page 2024-05-15 02:29:42 +02:00
f1c296c0d3 Added: array with variables on reviews page 2024-05-15 02:08:11 +02:00
2041c8998a Fixed: fetching review data, typos 2024-05-15 02:04:16 +02:00
cff010f2b4 Fix: review add overlapping dropdown 2024-05-15 01:46:08 +02:00
986aca3931 Fixed: review add - send add request to api 2024-05-15 01:35:52 +02:00
a57a059c2a Added: review add form 2024-05-15 01:05:25 +02:00
b7dc6af2e4 Added: images and review button on beer/get
Fixes: #12
2024-05-15 00:44:47 +02:00
4fc58aedd7 Added: folder with routes to review and add review page, added package RangeSlider; in _layout.js added add review as invisible 2024-05-14 22:49:11 +02:00
bf08eefe57 Fix: beer get page responsibility 2024-05-14 22:44:32 +02:00
6d6afc6274 Fixes: #25 again 2024-05-14 22:39:37 +02:00
82c90c1ceb Added: upload image in beer add form 2024-05-14 22:27:10 +02:00
4fe4808b3c Fix: image was rendering inside buttons view 2024-05-14 22:25:23 +02:00
b6b9b989c7 Edited: restyled beer get; Added: flashlist package 2024-05-14 22:25:23 +02:00
13b223ed5f Fixes: #25 2024-05-14 22:25:21 +02:00
04833277e1 Edited: .gitignore - added test-report.html 2024-05-14 19:41:35 +02:00
31179d7292 Fixes: #15 2024-05-12 23:38:17 +02:00
1c97262ee3 Added: tests for reviews, Reviews GET API endpoint; Fixed: tests for beers 2024-05-12 22:19:04 +02:00
bf791db47f Backend: code formatted 2024-05-12 22:19:04 +02:00
fa25015472 Added: package jest-html-reporter 2024-05-12 22:19:04 +02:00
4d02572d0c Added: API review 2024-05-12 22:19:04 +02:00
d418a53144 Fix: image was rendering inside buttons view 2024-05-12 21:58:18 +02:00
9b850ba88f Merge pull request 'kevin/add-form' (#14) from kevin/add-form into main
Beer add form
Reviewed-on: #14
2024-05-12 21:44:08 +02:00
41f52e67d3 Fix: responsivity 2024-05-12 21:43:05 +02:00
2d06e0fb12 Added: custom style for DropDownPicker, added icons for DropDownPicker, in style.js added new color for placeholder; Edited: Component for button, where you can now define background, border, ... by class 2024-05-12 21:26:44 +02:00
114ab257b2 Fix: changed dropdown menu to another package 2024-05-12 19:43:29 +02:00
f382150da3 Edited: Pick and take an image 2024-05-12 19:12:11 +02:00
630da41536 Fix: check that beer was added 2024-05-12 17:47:06 +02:00
674ee65c29 Added: check if beer was added, if not, show alert; Added packages picker,expo-camera 2024-05-12 12:10:32 +02:00
e8a0449ad2 Fix: gradient background on signup page; Added: color for text in button, on index page added new button and greeting 2024-05-09 20:06:20 +02:00
a25ce29154 Edited: README.md - how to build android package 2024-05-09 20:02:51 +02:00
6cf744bbc5 Added expo-linear-gradient and expo-system-ui package via npx expo install 2024-05-09 19:42:40 +02:00
7c25158c85 Edited: Styles on login page, styles on sign up page 2024-05-09 18:05:15 +02:00
05d0ff7134 Added: beer add page; all beers page 2024-05-09 15:32:26 +02:00
6497a05e3c Fix: add missing dependency 2024-05-08 23:41:29 +02:00
b388736ee8 Edited: Styles on login page 2024-05-08 23:22:05 +02:00
652a3b56d7 Added .env file 2024-05-08 21:30:30 +02:00
b17cf25970 Added: Link and Text component, Named links for components..., Tab layout 2024-05-08 21:18:47 +02:00
e62ef8695b Added biome for formatting, code formatted 2024-05-08 16:50:25 +02:00
dc446fe8dc Clean up 2024-05-08 16:37:48 +02:00
8e25ca5dd7 Moved from axios to Fetch API, Added user object to the AuthState 2024-05-08 16:31:21 +02:00
72 changed files with 4221 additions and 1981 deletions

View File

@@ -0,0 +1,32 @@
name: Build DeguApp backend
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Set up Node.js and TypeScript
uses: actions/setup-node@v1
with:
node-version: "20.14.0"
- name: npm install
working-directory: api/
run: |
npm install
- name: npm run build
working-directory: api/
run: |
npm run build --if-present
- name: npm run test
working-directory: api/
run: |
npm run test

View File

@@ -35,7 +35,12 @@ To get started with DeguApp, follow these steps:
2. Install dependencies: 2. Install dependencies:
```bash ```bash
cd deguapp # frontend
cd deguapp/frontend
npm install
# backend
cd deguapp/api
npm install npm install
``` ```
@@ -43,6 +48,45 @@ To get started with DeguApp, follow these steps:
5. Open the app in your browser or Android emulator and start exploring! 5. Open the app in your browser or Android emulator and start exploring!
## Local builds
### Android
```bash
cd frontend/
npm i
export ANDROID_HOME=$HOME/.Android/Sdk/
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
echo "EXPO_PUBLIC_API_URL=https://degu.filiprojek.cz/api/v1" > .env
npx expo prebuild
# edit gradle.properties and add info about signing key
# copy signing key to android/app/[keyname].keystore
# edit android/app/build.gradle
npx react-native build-android --mode=release
bundletool build-apks --bundle=./frontend/android/app/build/outputs/bundle/release/app-release.aab --output ./deguapp.apks --ks <upload-key.keystore> --ks-key-alias <upload-key-alias>
bundletool install-apks --apks=./deguapp.apks
```
#### Resources:
- https://github.com/expo/eas-cli/issues/1300
- https://reactnative.dev/docs/signed-apk-android#generating-the-release-aab
### Server
```bash
cd api/
npm i
npm run build
```
## Contributing ## Contributing
Contributions are welcome! If you'd like to contribute to DeguApp, please fork the repository and submit a pull request with your changes. Contributions are welcome! If you'd like to contribute to DeguApp, please fork the repository and submit a pull request with your changes.

View File

@@ -0,0 +1,23 @@
name: Build DeguApp backend
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Set up Node.js and TypeScript
uses: actions/setup-node@v1
with:
node-version: "20.14.0"
- name: npm install, build and test
run: |
npm install
npm run build --if-present
npm run test

2
api/.gitignore vendored
View File

@@ -2,3 +2,5 @@ node_modules/
dist/ dist/
package-lock.json package-lock.json
.env .env
test-report.html
uploads/

36
api/biome.json Normal file
View File

@@ -0,0 +1,36 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
"files": {
"ignore": [".expo/", ".vscode/", "node_modules/", "dist/", "*.json"]
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": false,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"ignore": [],
"attributePosition": "auto",
"indentStyle": "tab",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 120
},
"javascript": {
"formatter": {
"arrowParentheses": "always",
"bracketSameLine": true,
"bracketSpacing": true,
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"semicolons": "always",
"trailingComma": "all"
}
}
}

View File

@@ -59,6 +59,7 @@
"eslint": "^8.3.0", "eslint": "^8.3.0",
"http": "^0.0.1-security", "http": "^0.0.1-security",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-html-reporter": "^3.10.2",
"mocha": "^9.1.3", "mocha": "^9.1.3",
"mongodb-memory-server": "^9.2.0", "mongodb-memory-server": "^9.2.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
@@ -76,6 +77,12 @@
"testEnvironment": "node", "testEnvironment": "node",
"setupFilesAfterEnv": [ "setupFilesAfterEnv": [
"./tests/_setupFile.ts" "./tests/_setupFile.ts"
],
"reporters": [
"default",
["./node_modules/jest-html-reporter", {
"pageTitle": "Test Report"
}]
] ]
} }
} }

View File

@@ -1,29 +1,29 @@
import express from "express"; import express from "express";
import morgan from "morgan"; import morgan from "morgan";
import path from 'path' import path from "path";
import cors from 'cors' import cors from "cors";
import cookieParser from 'cookie-parser' import cookieParser from "cookie-parser";
import { router as routes } from "./routes"; import { router as routes } from "./routes";
//import { router as middlewares } from './middlewares' //import { router as middlewares } from './middlewares'
import env from './config/environment' import env from "./config/environment";
export let corsWhitelist: Array<string> export let corsWhitelist: Array<string>;
if (env.CORS_WHITELIST != 'undefined') { if (env.CORS_WHITELIST != "undefined") {
corsWhitelist = [...['http://localhost:8080', 'http://localhost:6040'], ...env.CORS_WHITELIST.split(';')] corsWhitelist = [...["http://localhost:8080", "http://localhost:6040"], ...env.CORS_WHITELIST.split(";")];
} else { } else {
corsWhitelist = ['http://localhost:8080', 'http://localhost:6040'] corsWhitelist = ["http://localhost:8080", "http://localhost:6040"];
} }
const corsOptions = { const corsOptions = {
origin: function (origin: any, callback: any) { origin: function (origin: any, callback: any) {
if (!origin || corsWhitelist.indexOf(origin) !== -1) { if (!origin || corsWhitelist.indexOf(origin) !== -1) {
callback(null, true) callback(null, true);
} else { } else {
callback(new Error('Not allowed by CORS')) callback(new Error("Not allowed by CORS"));
} }
}, },
optionsSuccessStatus: 200, optionsSuccessStatus: 200,
credentials: true credentials: true,
} };
export const app = express(); export const app = express();
@@ -31,12 +31,15 @@ export const app = express();
//app.use(middlewares) //app.use(middlewares)
//app.set('view engine', 'ejs') //app.set('view engine', 'ejs')
//app.set('views', path.join(__dirname, 'views')) //app.set('views', path.join(__dirname, 'views'))
app.use(cors(corsOptions)) app.use(cors(corsOptions));
app.use(morgan("dev")); app.use(morgan("dev"));
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.json()); app.use(express.json());
app.use(express.static(path.join(__dirname, 'public'))) app.use(express.static(path.join(__dirname, "public")));
app.use(cookieParser()) app.use('/public/uploads', express.static(path.join(__dirname, '../uploads')));
app.use(cookieParser());
// Routes // Routes
app.use(routes); app.use(routes);
//test

View File

@@ -1,13 +1,13 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import bcrypt from 'bcrypt'; import bcrypt from "bcrypt";
import jwt from 'jsonwebtoken'; import jwt from "jsonwebtoken";
import env from '../config/environment'; import env from "../config/environment";
import Docs from '../services/docsService'; import Docs from "../services/docsService";
import User from '../models/User'; import User from "../models/User";
import {Log} from 'nork' import { Log } from "nork";
import { IUser, signupExam, ISignin, signinExam } from '../validators/authValidator'; import { IUser, signupExam, ISignin, signinExam } from "../validators/authValidator";
new Docs('user', 'signup', '/api/v1/auth/signup', 'POST', 'user signup api', undefined, signupExam, 'status object'); new Docs("user", "signup", "/api/v1/auth/signup", "POST", "user signup api", undefined, signupExam, "status object");
export async function signup_post(req: Request, res: Response) { export async function signup_post(req: Request, res: Response) {
try { try {
const payload: IUser = req.body; const payload: IUser = req.body;
@@ -16,27 +16,27 @@ export async function signup_post(req: Request, res: Response) {
const user = new User(payload); const user = new User(payload);
await user.save(); await user.save();
res.status(201).json(Log.info(201, 'user was successfully signed up')); res.status(201).json(Log.info(201, "user was successfully signed up"));
} catch (err: any) { } catch (err: any) {
if (err.code == 11000) { if (err.code == 11000) {
res.status(400).json(Log.error(400, 'this user already exists')); res.status(400).json(Log.error(400, "this user already exists"));
return; return;
} }
Log.error(500, err); Log.error(500, err);
res.status(500).json(Log.error(500, 'something went wrong')); res.status(500).json(Log.error(500, "something went wrong"));
} }
} }
new Docs('user', 'signin', '/api/v1/auth/signin', 'POST', 'user signin api', undefined, signinExam, 'status object'); new Docs("user", "signin", "/api/v1/auth/signin", "POST", "user signin api", undefined, signinExam, "status object");
export async function signin_post(req: Request, res: Response) { export async function signin_post(req: Request, res: Response) {
try { try {
const payload: ISignin = req.body; const payload: ISignin = req.body;
const user = await User.findOne({ email: payload.email }); const user = await User.findOne({ email: payload.email });
if (!user) { if (!user) {
res.cookie('jwt', '', { httpOnly: true, maxAge: 0 }); res.cookie("jwt", "", { httpOnly: true, maxAge: 0 });
res.cookie('auth', false, { httpOnly: false, maxAge: 0 }); res.cookie("auth", false, { httpOnly: false, maxAge: 0 });
res.status(401).json(Log.error(401, 'email or password is wrong')); res.status(401).json(Log.error(401, "email or password is wrong"));
return; return;
} }
@@ -44,38 +44,47 @@ export async function signin_post(req: Request, res: Response) {
const maxAge = 3 * 24 * 60 * 60; // 3 days in seconds const maxAge = 3 * 24 * 60 * 60; // 3 days in seconds
const createToken = (id: any) => { const createToken = (id: any) => {
return jwt.sign({ id }, env.JWT_SECRET, { return jwt.sign({ id }, env.JWT_SECRET, {
expiresIn: maxAge expiresIn: maxAge,
}); });
}; };
const token = createToken(user._id); const token = createToken(user._id);
res.cookie('jwt', token, { httpOnly: true, maxAge: maxAge * 1000 }); res.cookie("jwt", token, { httpOnly: true, maxAge: maxAge * 1000 });
res.cookie('auth', true, { httpOnly: false, maxAge: maxAge * 1000 }); res.cookie("auth", true, { httpOnly: false, maxAge: maxAge * 1000 });
res.json(Log.info(200, 'user is logged in', {jwt: token})); res.json(Log.info(200, "user is logged in", { jwt: token }));
return; return;
} }
res.cookie('jwt', '', { httpOnly: true, maxAge: 0 }); res.cookie("jwt", "", { httpOnly: true, maxAge: 0 });
res.cookie('auth', false, { httpOnly: false, maxAge: 0 }); res.cookie("auth", false, { httpOnly: false, maxAge: 0 });
res.status(401).json(Log.error(401, 'email or password is wrong')); res.status(401).json(Log.error(401, "email or password is wrong"));
} catch (err: any) { } catch (err: any) {
Log.error(500, err); Log.error(500, err);
res.status(500).json(Log.error(500, 'something went wrong')); res.status(500).json(Log.error(500, "something went wrong"));
} }
} }
new Docs('user', 'logout', '/api/v1/auth/logout', 'POST', 'user logout api', undefined, {}, 'status object'); new Docs("user", "logout", "/api/v1/auth/logout", "POST", "user logout api", undefined, {}, "status object");
export function logout_post(req: Request, res: Response) { export function logout_post(req: Request, res: Response) {
res.cookie('jwt', '', { httpOnly: true, maxAge: 0 }); res.cookie("jwt", "", { httpOnly: true, maxAge: 0 });
res.cookie('auth', false, { httpOnly: false, maxAge: 0 }); res.cookie("auth", false, { httpOnly: false, maxAge: 0 });
res.json(Log.info(200, 'user was logged out')); res.json(Log.info(200, "user was logged out"));
} }
new Docs('user', 'status', '/api/v1/auth/status', 'GET', 'user login status api', undefined, undefined, 'status code | user object'); new Docs(
"user",
"status",
"/api/v1/auth/status",
"GET",
"user login status api",
undefined,
undefined,
"status code | user object",
);
export function status_get(req: Request, res: Response) { export function status_get(req: Request, res: Response) {
let userObject = res.locals.user; let userObject = res.locals.user;
userObject.password = undefined; userObject.password = undefined;
userObject.__v = undefined; userObject.__v = undefined;
res.status(200).json(Log.info(200, 'user is logged in', userObject)); res.status(200).json(Log.info(200, "user is logged in", userObject));
} }

View File

@@ -1,13 +1,22 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import Beer from '../models/Beer'; import Beer from "../models/Beer";
import { isValidObjectId, Types } from 'mongoose'; import { isValidObjectId, Types } from "mongoose";
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
import {Log} from 'nork' import { Log } from "nork";
import Docs from '../services/docsService'; import Docs from "../services/docsService";
import { addExam, delExam, editExam, IBeer } from '../validators/beerValidator'; import { addExam, delExam, editExam, IBeer } from "../validators/beerValidator";
new Docs('beer', 'add', '/api/v1/beer/add', 'POST', 'beer add api', undefined, { ...addExam, photos: 'optional field | max 4 images | formData' }, 'status object | beer object'); new Docs(
"beer",
"add",
"/api/v1/beer/add",
"POST",
"beer add api",
undefined,
{ ...addExam, photos: "optional field | max 4 images | formData" },
"status object | beer object",
);
export async function add_post(req: Request, res: Response) { export async function add_post(req: Request, res: Response) {
try { try {
if (req.files) { if (req.files) {
@@ -19,28 +28,37 @@ export async function add_post(req: Request, res: Response) {
} }
const beer = new Beer(req.body); const beer = new Beer(req.body);
await beer.save(); await beer.save();
res.status(201).json(Log.info(201, 'beer was created', beer)); res.status(201).json(Log.info(201, "beer was created", beer));
} catch (err: any) { } catch (err: any) {
Log.error(500, 'error in add_post', err); Log.error(500, "error in add_post", err);
res.status(500).json(Log.error(500, 'something went wrong')); res.status(500).json(Log.error(500, "something went wrong"));
} }
} }
new Docs('beer', 'get', '/api/v1/beer/get', 'GET', 'beer get api', undefined, undefined, 'status object | array of beer objects'); new Docs(
"beer",
"get",
"/api/v1/beer/get",
"GET",
"beer get api",
undefined,
undefined,
"status object | array of beer objects",
);
export async function get_get(req: Request, res: Response) { export async function get_get(req: Request, res: Response) {
try { try {
const beer = await Beer.find({}, '-__v'); const beer = await Beer.find({}, "-__v");
res.status(200).json(Log.info(200, 'beers fetched', beer)); res.status(200).json(Log.info(200, "beers fetched", beer));
} catch (err: any) { } catch (err: any) {
Log.error(500, 'error in get_get', err); Log.error(500, "error in get_get", err);
res.status(500).json(Log.error(500, 'something went wrong')); res.status(500).json(Log.error(500, "something went wrong"));
} }
} }
new Docs('beer', 'del', '/api/v1/beer/del', 'POST', 'beer del api', undefined, delExam, 'status object'); new Docs("beer", "del", "/api/v1/beer/del", "POST", "beer del api", undefined, delExam, "status object");
export async function del_post(req: Request, res: Response) { export async function del_post(req: Request, res: Response) {
try { try {
if (!isValidObjectId(req.body._id)) throw Log.error(400, 'this is not valid _id'); if (!isValidObjectId(req.body._id)) throw Log.error(400, "this is not valid _id");
const beer = await Beer.deleteOne(new Types.ObjectId(req.body._id)); const beer = await Beer.deleteOne(new Types.ObjectId(req.body._id));
@@ -54,30 +72,39 @@ export async function del_post(req: Request, res: Response) {
res.status(err.code).json(err); res.status(err.code).json(err);
return; return;
} }
Log.error(500, 'error in del_post', err); Log.error(500, "error in del_post", err);
res.status(500).json(Log.error(500, 'something went wrong')); res.status(500).json(Log.error(500, "something went wrong"));
} }
} }
new Docs('beer', 'edit', '/api/v1/beer/edit', 'POST', 'beer edit api', undefined, { ...editExam, photos: 'optional field | max 4 images | formData' }, 'status object | beer data'); new Docs(
"beer",
"edit",
"/api/v1/beer/edit",
"POST",
"beer edit api",
undefined,
{ ...editExam, photos: "optional field | max 4 images | formData" },
"status object | beer data",
);
export async function edit_post(req: Request, res: Response) { export async function edit_post(req: Request, res: Response) {
try { try {
if (!isValidObjectId(req.body._id)) throw Log.error(400, 'this is not valid _id'); if (!isValidObjectId(req.body._id)) throw Log.error(400, "this is not valid _id");
if (req.files) { if (req.files) {
if (!req.body.imgs) { if (!req.body.imgs) {
req.body.imgs = []; req.body.imgs = [];
} }
if (typeof req.body.imgs == 'string') { if (typeof req.body.imgs == "string") {
req.body.imgs = [req.body.imgs]; req.body.imgs = [req.body.imgs];
} }
if (req.body.imgs.length + req.files.length > 4) { if (req.body.imgs.length + req.files.length > 4) {
req.body.imgs.forEach((el: string[]) => { req.body.imgs.forEach((el: string[]) => {
fs.rmSync(path.join(__dirname, '../../uploads/' + el)); fs.rmSync(path.join(__dirname, "../../uploads/" + el));
}); });
throw Log.error(400, 'exceeds the 4 image limit'); throw Log.error(400, "exceeds the 4 image limit");
} }
const files: any = req.files; const files: any = req.files;
@@ -90,11 +117,11 @@ export async function edit_post(req: Request, res: Response) {
const beer = await Beer.findOneAndUpdate(new Types.ObjectId(req.body._id), payload, { new: true }); const beer = await Beer.findOneAndUpdate(new Types.ObjectId(req.body._id), payload, { new: true });
res.json(Log.info(200, `beer ${req.body._id} edited`, beer)); res.json(Log.info(200, `beer ${req.body._id} edited`, beer));
} catch (err: any) { } catch (err: any) {
if (err.code && typeof err.code == 'number') { if (err.code && typeof err.code == "number") {
res.status(err.code).json(err); res.status(err.code).json(err);
return; return;
} }
Log.error(500, 'error in del_post', err); Log.error(500, "error in del_post", err);
res.status(500).json(Log.error(500, 'something went wrong')); res.status(500).json(Log.error(500, "something went wrong"));
} }
} }

View File

@@ -1,14 +1,14 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import path from 'path'; import path from "path";
import fs from 'fs'; import fs from "fs";
import {Log} from 'nork' import { Log } from "nork";
import { Docs } from '../services/docsService'; import { Docs } from "../services/docsService";
new Docs('docs', 'get_all', '/api/v1', 'GET', 'Get docs json', undefined, undefined, 'docs json'); new Docs("docs", "get_all", "/api/v1", "GET", "Get docs json", undefined, undefined, "docs json");
export function docs_get(req: Request, res: Response) { export function docs_get(req: Request, res: Response) {
try { try {
res.json(JSON.parse(fs.readFileSync(path.join(__dirname, '../public/api.json')).toString())); res.json(JSON.parse(fs.readFileSync(path.join(__dirname, "../public/api.json")).toString()));
} catch (err: any) { } catch (err: any) {
res.status(500).json(Log.error(500, 'api.json docs file does not exists under public folder')); res.status(500).json(Log.error(500, "api.json docs file does not exists under public folder"));
} }
} }

View File

@@ -0,0 +1,71 @@
import { Request, Response } from "express";
import Review from "../models/Review";
import { isValidObjectId, Types } from "mongoose";
import { Log } from "nork";
import Docs from "../services/docsService";
import { addExam, delExam, IReview } from "../validators/reviewValidator";
new Docs(
"review",
"add",
"/api/v1/review/add",
"POST",
"review add api",
undefined,
addExam,
"status object | review object",
);
export async function add_post(req: Request, res: Response) {
try {
const data: IReview = req.body;
data.user_id = res.locals.user._id
const review = new Review(data);
await review.save();
res.status(201).json(Log.info(201, "review was added", review));
} catch (err) {
Log.error(500, "error while adding review", err);
res.status(500).json(Log.error(500, "something went wrong"));
}
}
new Docs(
"review",
"get",
"/api/v1/review/get",
"GET",
"review get api",
undefined,
undefined,
"status object | array of review objects",
);
export async function get_get(req: Request, res: Response) {
try {
const review = await Review.find({}, "-__v");
res.status(200).json(Log.info(200, "reviews fetched", review));
} catch (err) {
Log.error(500, "error while geting reviews", err);
res.status(500).json(Log.error(500, "something went wrong"));
}
}
new Docs("review", "del", "/api/v1/review/del", "POST", "review del api", undefined, delExam, "status object");
export async function del_post(req: Request, res: Response) {
try {
if (!isValidObjectId(req.body._id)) throw Log.error(400, "this is not valid _id");
const review = await Review.deleteOne(new Types.ObjectId(req.body._id));
if (review.deletedCount > 0) {
res.status(200).json(Log.info(200, `review ${req.body._id} deleted`));
return;
}
throw Log.error(400, `review ${req.body._id} does not exist`);
} catch (err: any) {
if (err.code) {
res.status(err.code).json(err);
return;
}
Log.error(500, "error in del_post", err);
res.status(500).json(Log.error(500, "something went wrong"));
}
}

View File

@@ -1,9 +1,9 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import jwt from 'jsonwebtoken'; import jwt from "jsonwebtoken";
import env from '../config/environment'; import env from "../config/environment";
import { Log } from 'nork'; import { Log } from "nork";
import User from '../models/User'; import User from "../models/User";
import { isValidObjectId } from 'mongoose'; import { isValidObjectId } from "mongoose";
export function requireAuth(req: Request, res: Response, next: NextFunction) { export function requireAuth(req: Request, res: Response, next: NextFunction) {
const token = req.cookies?.jwt; const token = req.cookies?.jwt;
@@ -12,40 +12,40 @@ export function requireAuth(req: Request, res: Response, next: NextFunction) {
jwt.verify(token, env.JWT_SECRET, async (err: any, decodedToken: any) => { jwt.verify(token, env.JWT_SECRET, async (err: any, decodedToken: any) => {
if (err) { if (err) {
// console.error(err.message) // console.error(err.message)
res.status(401).send(Log.error(401, 'user is not authenticated')); res.status(401).send(Log.error(401, "user is not authenticated"));
} }
if (!err) { if (!err) {
const user = await User.findById(decodedToken.id); const user = await User.findById(decodedToken.id);
if (user === null) { if (user === null) {
res.status(401).send(Log.error(401, 'user is not authenticated')); res.status(401).send(Log.error(401, "user is not authenticated"));
return; return;
} }
res.locals.user = user; res.locals.user = user;
Log.info(100, 'user is authenticated'); Log.info(100, "user is authenticated");
next(); next();
} }
}); });
} }
if (!token) { if (!token) {
res.status(401).send(Log.error(401, 'user is not authenticated')); res.status(401).send(Log.error(401, "user is not authenticated"));
} }
} }
export function requireVerified(req: Request, res: Response, next: NextFunction) { export function requireVerified(req: Request, res: Response, next: NextFunction) {
if (res.locals.user._id) { if (res.locals.user._id) {
if (res.locals.user.verified) { if (res.locals.user.verified) {
Log.info(100, 'user is verified'); Log.info(100, "user is verified");
next(); next();
return; return;
} }
res.status(403).json(Log.error(403, 'user is not verified')); res.status(403).json(Log.error(403, "user is not verified"));
return; return;
} }
if (!res.locals.user._id) { if (!res.locals.user._id) {
res.status(401).send(Log.error(401, 'user is not authenticated')); res.status(401).send(Log.error(401, "user is not authenticated"));
return; return;
} }
} }
@@ -53,33 +53,33 @@ export function requireVerified(req: Request, res: Response, next: NextFunction)
export class requireRole { export class requireRole {
static Admin(req: Request, res: Response, next: NextFunction) { static Admin(req: Request, res: Response, next: NextFunction) {
if (res.locals.user.admin) { if (res.locals.user.admin) {
Log.info(100, 'user is admin'); Log.info(100, "user is admin");
next(); next();
return; return;
} }
res.status(403).json(Log.error(403, 'insufficient permissions')); res.status(403).json(Log.error(403, "insufficient permissions"));
return; return;
} }
static Owner(req: Request, res: Response, next: NextFunction) { static Owner(req: Request, res: Response, next: NextFunction) {
try { try {
if (!isValidObjectId(req.body.domain_id)) { if (!isValidObjectId(req.body.domain_id)) {
throw Log.error(400, 'neznámé domain_id'); throw Log.error(400, "neznámé domain_id");
} }
const domain = res.locals.user.domains.filter((domain: any) => domain.domain_id == req.body.domain_id); const domain = res.locals.user.domains.filter((domain: any) => domain.domain_id == req.body.domain_id);
console.log(domain); console.log(domain);
if (domain.length < 1) { if (domain.length < 1) {
throw Log.error(400, 'neznámé domain_id'); throw Log.error(400, "neznámé domain_id");
} }
if (domain[0].role == 1) { if (domain[0].role == 1) {
Log.info(100, 'user is owner'); Log.info(100, "user is owner");
next(); next();
return; return;
} }
res.status(403).json(Log.error(403, 'insufficient permissions')); res.status(403).json(Log.error(403, "insufficient permissions"));
return; return;
} catch (err: any) { } catch (err: any) {
res.status(400).json(err); res.status(400).json(err);
@@ -87,4 +87,4 @@ export class requireRole {
} }
static Editor(req: Request, res: Response, next: NextFunction) {} static Editor(req: Request, res: Response, next: NextFunction) {}
static Guest(req: Request, res: Response, next: NextFunction) {} static Guest(req: Request, res: Response, next: NextFunction) {}
} }

View File

@@ -1,11 +1,11 @@
import multer from 'multer'; import multer from "multer";
import {Log} from 'nork' import { Log } from "nork";
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
const validateMulterRequestMiddleware = async (err: any, req: Request, res: Response, next: NextFunction) => { const validateMulterRequestMiddleware = async (err: any, req: Request, res: Response, next: NextFunction) => {
if (err instanceof multer.MulterError) { if (err instanceof multer.MulterError) {
Log.error(500, 'error while processing uploaded files', JSON.stringify(err)); Log.error(500, "error while processing uploaded files", JSON.stringify(err));
res.status(400).json(Log.error(400, 'error while processing uploaded files')); res.status(400).json(Log.error(400, "error while processing uploaded files"));
return; return;
} else { } else {
next(); next();

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { object, AnySchema } from 'yup'; import { object, AnySchema } from "yup";
import { Log } from 'nork'; import { Log } from "nork";
const validate = (schema: AnySchema) => async (req: Request, res: Response, next: NextFunction) => { const validate = (schema: AnySchema) => async (req: Request, res: Response, next: NextFunction) => {
try { try {
@@ -8,7 +8,7 @@ const validate = (schema: AnySchema) => async (req: Request, res: Response, next
next(); next();
} catch (err: any) { } catch (err: any) {
return res.status(400).json(Log.error(400, 'validation error', err)); return res.status(400).json(Log.error(400, "validation error", err));
} }
}; };

View File

@@ -1,34 +1,34 @@
import path from 'path'; import path from "path";
import { Schema, model } from 'mongoose'; import { Schema, model } from "mongoose";
import { IBeer } from '../validators/beerValidator'; import { IBeer } from "../validators/beerValidator";
const schema = new Schema<IBeer | any>( const schema = new Schema<IBeer | any>(
{ {
name: { name: {
type: String, type: String,
required: true required: true,
}, },
degree: { degree: {
type: Number, type: Number,
required: true required: true,
}, },
packaging: { packaging: {
type: String, type: String,
required: true required: true,
}, },
brand: { brand: {
type: String, type: String,
required: true required: true,
}, },
imgs: { imgs: {
type: Array, type: Array,
required: false, required: false,
default: [] default: [],
} },
}, },
{ {
timestamps: true timestamps: true,
} },
); );
export default model(path.basename(__filename).split('.')[0], schema); export default model(path.basename(__filename).split(".")[0], schema);

45
api/src/models/Review.ts Normal file
View File

@@ -0,0 +1,45 @@
import path from "path";
import { Schema, model } from "mongoose";
import { IReview } from "../validators/reviewValidator";
const schema = new Schema<IReview | any>(
{
foam: {
type: Number,
required: true,
},
bitter_sweetness: {
type: Number,
required: true,
},
taste: {
type: Number,
required: true,
},
packaging: {
type: Number,
required: true,
},
sourness: {
type: Boolean,
required: true,
},
would_again: {
type: Boolean,
required: true,
},
beer_id: {
type: String,
required: true,
},
user_id: {
type: String,
required: true,
}
},
{
timestamps: true,
},
);
export default model(path.basename(__filename).split(".")[0], schema);

View File

@@ -1,26 +1,26 @@
import path from 'path'; import path from "path";
import { Schema, model } from 'mongoose'; import { Schema, model } from "mongoose";
import { IUser } from '../validators/authValidator'; import { IUser } from "../validators/authValidator";
const schema = new Schema<IUser>( const schema = new Schema<IUser>(
{ {
username: { username: {
type: String, type: String,
required: true required: true,
}, },
email: { email: {
type: String, type: String,
required: true, required: true,
unique: true unique: true,
}, },
password: { password: {
type: String, type: String,
required: true required: true,
} },
}, },
{ {
timestamps: true timestamps: true,
} },
); );
export default model(path.basename(__filename).split('.')[0], schema); export default model(path.basename(__filename).split(".")[0], schema);

View File

@@ -1 +1 @@
{"version":"2.0.0","endpoints":{"user":{"signup":{"name":"user","operation":"signup","route":"/api/v1/auth/signup","method":"POST","description":"user signup api","body":{"username":"testuser","email":"text@example.com","password":"Test1234"},"response":"status object"},"signin":{"name":"user","operation":"signin","route":"/api/v1/auth/signin","method":"POST","description":"user signin api","body":{"email":"text@example.com","password":"Test1234"},"response":"status object"},"logout":{"name":"user","operation":"logout","route":"/api/v1/auth/logout","method":"POST","description":"user logout api","body":{},"response":"status object"},"status":{"name":"user","operation":"status","route":"/api/v1/auth/status","method":"GET","description":"user login status api","response":"status code | user object"}},"beer":{"add":{"name":"beer","operation":"add","route":"/api/v1/beer/add","method":"POST","description":"beer add api","body":{"brand":"Pilsner Urqell","name":"Kozel","degree":11,"packaging":"can","photos":"optional field | max 4 images | formData"},"response":"status object | beer object"},"get":{"name":"beer","operation":"get","route":"/api/v1/beer/get","method":"GET","description":"beer get api","response":"status object | array of beer objects"},"del":{"name":"beer","operation":"del","route":"/api/v1/beer/del","method":"POST","description":"beer del api","body":{"_id":"6352b303b71cb62222f39895"},"response":"status object"},"edit":{"name":"beer","operation":"edit","route":"/api/v1/beer/edit","method":"POST","description":"beer edit api","body":{"_id":"6355b95dc03fad77bc380146","brand":"Pilsner Urqell","name":"Radegast","degree":12,"packaging":"bottle","imgs":[],"photos":"optional field | max 4 images | formData"},"response":"status object | beer data"}},"docs":{"get_all":{"name":"docs","operation":"get_all","route":"/api/v1","method":"GET","description":"Get docs json","response":"docs json"}}}} {"version":"2.0.0","endpoints":{"user":{"signup":{"name":"user","operation":"signup","route":"/api/v1/auth/signup","method":"POST","description":"user signup api","body":{"username":"testuser","email":"text@example.com","password":"Test1234"},"response":"status object"},"signin":{"name":"user","operation":"signin","route":"/api/v1/auth/signin","method":"POST","description":"user signin api","body":{"email":"text@example.com","password":"Test1234"},"response":"status object"},"logout":{"name":"user","operation":"logout","route":"/api/v1/auth/logout","method":"POST","description":"user logout api","body":{},"response":"status object"},"status":{"name":"user","operation":"status","route":"/api/v1/auth/status","method":"GET","description":"user login status api","response":"status code | user object"}},"beer":{"add":{"name":"beer","operation":"add","route":"/api/v1/beer/add","method":"POST","description":"beer add api","body":{"brand":"Pilsner Urqell","name":"Kozel","degree":11,"packaging":"can","photos":"optional field | max 4 images | formData"},"response":"status object | beer object"},"get":{"name":"beer","operation":"get","route":"/api/v1/beer/get","method":"GET","description":"beer get api","response":"status object | array of beer objects"},"del":{"name":"beer","operation":"del","route":"/api/v1/beer/del","method":"POST","description":"beer del api","body":{"_id":"6352b303b71cb62222f39895"},"response":"status object"},"edit":{"name":"beer","operation":"edit","route":"/api/v1/beer/edit","method":"POST","description":"beer edit api","body":{"_id":"6355b95dc03fad77bc380146","brand":"Pilsner Urqell","name":"Radegast","degree":12,"packaging":"bottle","imgs":[],"photos":"optional field | max 4 images | formData"},"response":"status object | beer data"}},"docs":{"get_all":{"name":"docs","operation":"get_all","route":"/api/v1","method":"GET","description":"Get docs json","response":"docs json"}},"review":{"add":{"name":"review","operation":"add","route":"/api/v1/review/add","method":"POST","description":"review add api","body":{"beer_id":"6352b303b71cb62222f39895","foam":3,"bitter_sweetness":2,"taste":5,"packaging":3,"sourness":false,"would_again":true},"response":"status object | review object"},"get":{"name":"review","operation":"get","route":"/api/v1/review/get","method":"GET","description":"review get api","response":"status object | array of review objects"},"del":{"name":"review","operation":"del","route":"/api/v1/review/del","method":"POST","description":"review del api","body":{"_id":"6352b303b71cb62222f39895"},"response":"status object"}}}}

View File

@@ -1,30 +1,42 @@
import { Router } from "express"; import { Router } from "express";
import multer from 'multer'; import multer from "multer";
import path from 'path' import path from "path";
import * as authController from "../controllers/authController"; import * as authController from "../controllers/authController";
import * as beerController from "../controllers/beerController" import * as beerController from "../controllers/beerController";
import * as docsController from "../controllers/docsController" import * as docsController from "../controllers/docsController";
import * as reviewController from "../controllers/reviewController";
import { requireAuth } from "../middlewares/authMiddleware"; import { requireAuth } from "../middlewares/authMiddleware";
import validate from '../middlewares/validateRequest' import validate from "../middlewares/validateRequest";
import valMulter from '../middlewares/validateMulterRequest'; import valMulter from "../middlewares/validateMulterRequest";
import * as AuthVal from '../validators/authValidator' import * as AuthVal from "../validators/authValidator";
import * as BVal from '../validators/beerValidator'; import * as BVal from "../validators/beerValidator";
import * as RVal from "../validators/reviewValidator";
const upload = multer({ dest: path.resolve(__dirname, '../../uploads') }); const upload = multer({ dest: path.resolve(__dirname, "../../uploads") });
const router = Router(); const router = Router();
router.get('/', docsController.docs_get); router.get("/", docsController.docs_get);
router.post("/auth/signup",validate(AuthVal.signup) , authController.signup_post); router.post("/auth/signup", validate(AuthVal.signup), authController.signup_post);
router.post("/auth/signin",validate(AuthVal.signin) , authController.signin_post); router.post("/auth/signin", validate(AuthVal.signin), authController.signin_post);
router.options("/auth/signin",validate(AuthVal.signin) , authController.signin_post);
router.post("/auth/logout", requireAuth, authController.logout_post); router.post("/auth/logout", requireAuth, authController.logout_post);
router.get("/auth/status", requireAuth, authController.status_get); router.get("/auth/status", requireAuth, authController.status_get);
router.post('/beer/add', [requireAuth, upload.array('photos', 4), valMulter, validate(BVal.add)], beerController.add_post); router.post(
router.get('/beer/get', [requireAuth], beerController.get_get); "/beer/add",
router.post('/beer/del', [requireAuth, validate(BVal.del)], beerController.del_post); [requireAuth, upload.array("photos", 4), valMulter, validate(BVal.add)],
router.post('/beer/edit', [requireAuth, upload.array('photos', 4), valMulter, validate(BVal.edit)], beerController.edit_post); beerController.add_post,
);
router.get("/beer/get", [requireAuth], beerController.get_get);
router.post("/beer/del", [requireAuth, validate(BVal.del)], beerController.del_post);
router.post(
"/beer/edit",
[requireAuth, upload.array("photos", 4), valMulter, validate(BVal.edit)],
beerController.edit_post,
);
export default router; router.post("/review/add", [requireAuth, validate(RVal.add)], reviewController.add_post);
router.get("/review/get", requireAuth, reviewController.get_get);
export default router;

View File

@@ -5,11 +5,11 @@ export const router = Router();
router.use("/api/v1", api_v1); router.use("/api/v1", api_v1);
//router.get("*", (req: Request, res: Response) => { router.get("*", (req: Request, res: Response) => {
// res.sendFile(path.join(__dirname, "../views/index.html")); res.sendFile(path.join(__dirname, "../public/index.html"));
//}); });
// 404 // 404
router.use((req: Request, res: Response) => { router.use((req: Request, res: Response) => {
res.status(404).send("Error 404\n"); res.status(404).send("Error 404\n");
}); });

View File

@@ -1,7 +1,7 @@
import http from "http"; import http from "http";
import { app } from "./app"; import { app } from "./app";
import env from "./config/environment"; import env from "./config/environment";
import mongoose from 'mongoose' // TODO: dopsat nork module pro db import mongoose from "mongoose"; // TODO: dopsat nork module pro db
import { Log } from "nork"; import { Log } from "nork";
const port: number = env.APP_PORT || 8080; const port: number = env.APP_PORT || 8080;
@@ -23,13 +23,11 @@ export const server = http.createServer(app);
// runServer() // runServer()
//} //}
(async () => { (async () => {
if (!process.env.DOCS_GEN) { if (!process.env.DOCS_GEN) {
try { try {
await mongoose.connect(env.DB_URI); await mongoose.connect(env.DB_URI);
Log.info(200, 'connected to db'); Log.info(200, "connected to db");
server.listen(port, () => { server.listen(port, () => {
Log.info(200, `Server is listening on http://localhost:${port}`); Log.info(200, `Server is listening on http://localhost:${port}`);
}); });

View File

@@ -1,8 +1,8 @@
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
export type HttpMethods = 'POST' | 'GET' | 'PUT' | 'DELETE'; export type HttpMethods = "POST" | "GET" | "PUT" | "DELETE";
export type ApiResponse = 'status object' | Object | string; export type ApiResponse = "status object" | Object | string;
export class Docs { export class Docs {
private name: string; private name: string;
@@ -14,7 +14,16 @@ export class Docs {
private body?: Object; private body?: Object;
private response?: ApiResponse; private response?: ApiResponse;
public constructor(name: string, operation: string, route: string, method: HttpMethods, description?: string, parameters?: Object[], body?: Object, response?: ApiResponse) { public constructor(
name: string,
operation: string,
route: string,
method: HttpMethods,
description?: string,
parameters?: Object[],
body?: Object,
response?: ApiResponse,
) {
this.name = name; this.name = name;
this.operation = operation; this.operation = operation;
this.route = route; this.route = route;
@@ -32,14 +41,14 @@ export class Docs {
return false; return false;
} }
const jsonPath = path.join(__dirname, '../public/api.json'); const jsonPath = path.join(__dirname, "../public/api.json");
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString()); const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json")).toString());
const jsonAPI = () => JSON.parse(fs.readFileSync(jsonPath).toString()); const jsonAPI = () => JSON.parse(fs.readFileSync(jsonPath).toString());
const genJsonAPI = () => fs.writeFileSync(jsonPath, JSON.stringify({ version: pkg.version, endpoints: {} })); const genJsonAPI = () => fs.writeFileSync(jsonPath, JSON.stringify({ version: pkg.version, endpoints: {} }));
if (!fs.existsSync(path.join(__dirname, '../public'))) { if (!fs.existsSync(path.join(__dirname, "../public"))) {
fs.mkdirSync(path.join(__dirname, '../public')); fs.mkdirSync(path.join(__dirname, "../public"));
} }
if (!fs.existsSync(jsonPath)) { if (!fs.existsSync(jsonPath)) {
@@ -59,4 +68,4 @@ export class Docs {
} }
} }
export default Docs; export default Docs;

View File

@@ -1,6 +1,6 @@
import * as shell from 'shelljs'; import * as shell from "shelljs";
// Copy all the view templates // Copy all the view templates
//shell.cp('-R', 'src/views', 'dist/') //shell.cp('-R', 'src/views', 'dist/')
shell.cp('-R', 'src/public', 'dist/'); shell.cp("-R", "src/public", "dist/");
shell.cp('-u', 'src/.env', 'dist/'); shell.cp("-u", "src/.env", "dist/");

View File

@@ -1,10 +1,10 @@
const mongoose = require('mongoose'); const mongoose = require("mongoose");
const { MongoMemoryServer } = require('mongodb-memory-server'); const { MongoMemoryServer } = require("mongodb-memory-server");
let mongo: any = null; let mongo: any = null;
const connectDB = async () => { const connectDB = async () => {
mongo = await MongoMemoryServer.create({ binary: { os: { os: 'linux', dist: 'ubuntu', release: '18.04' } } }); // TODO: check that host OS is Void Linux, else remove the argument mongo = await MongoMemoryServer.create({ binary: { os: { os: "linux", dist: "ubuntu", release: "18.04" } } }); // TODO: check that host OS is Void Linux, else remove the argument
const uri = mongo.getUri(); const uri = mongo.getUri();
await mongoose.connect(uri); await mongoose.connect(uri);
@@ -27,4 +27,4 @@ const dropCollections = async () => {
} }
}; };
export { connectDB, dropDB, dropCollections }; export { connectDB, dropDB, dropCollections };

View File

@@ -1,7 +1,7 @@
import * as yup from 'yup'; import * as yup from "yup";
import YupPassword from 'yup-password'; import YupPassword from "yup-password";
YupPassword(yup); YupPassword(yup);
import { Schema } from 'mongoose'; import { Schema } from "mongoose";
interface mongooseAddition { interface mongooseAddition {
_id?: Schema.Types.ObjectId; _id?: Schema.Types.ObjectId;
@@ -13,22 +13,22 @@ interface mongooseAddition {
export const signup = yup.object({ export const signup = yup.object({
username: yup.string().required(), username: yup.string().required(),
email: yup.string().email().required(), email: yup.string().email().required(),
password: yup.string().min(8).minLowercase(1).minUppercase(1).minNumbers(1).required() password: yup.string().min(8).minLowercase(1).minUppercase(1).minNumbers(1).required(),
}); });
export interface IUser extends yup.InferType<typeof signup>, mongooseAddition {} export interface IUser extends yup.InferType<typeof signup>, mongooseAddition {}
export const signupExam: IUser = { export const signupExam: IUser = {
username: 'testuser', username: "testuser",
email: 'text@example.com', email: "text@example.com",
password: 'Test1234' password: "Test1234",
}; };
// SignIn // SignIn
export const signin = yup.object({ export const signin = yup.object({
email: yup.string().email().required(), email: yup.string().email().required(),
password: yup.string().min(8).minLowercase(1).minUppercase(1).minNumbers(1).required() password: yup.string().min(8).minLowercase(1).minUppercase(1).minNumbers(1).required(),
}); });
export interface ISignin extends yup.InferType<typeof signin>, mongooseAddition {} export interface ISignin extends yup.InferType<typeof signin>, mongooseAddition {}
export const signinExam: ISignin = { export const signinExam: ISignin = {
email: 'text@example.com', email: "text@example.com",
password: 'Test1234' password: "Test1234",
}; };

View File

@@ -1,7 +1,7 @@
import * as yup from 'yup'; import * as yup from "yup";
import YupPassword from 'yup-password'; import YupPassword from "yup-password";
YupPassword(yup); YupPassword(yup);
import { Schema } from 'mongoose'; import { Schema } from "mongoose";
interface mongooseAddition { interface mongooseAddition {
_id?: Schema.Types.ObjectId; _id?: Schema.Types.ObjectId;
@@ -14,23 +14,23 @@ export const add = yup.object({
brand: yup.string().required(), brand: yup.string().required(),
name: yup.string().required(), name: yup.string().required(),
degree: yup.number().required(), degree: yup.number().required(),
packaging: yup.string().required() packaging: yup.string().required(),
}); });
export interface IBeer extends yup.InferType<typeof add>, mongooseAddition {} export interface IBeer extends yup.InferType<typeof add>, mongooseAddition {}
export const addExam: IBeer = { export const addExam: IBeer = {
brand: 'Pilsner Urqell', brand: "Pilsner Urqell",
name: 'Kozel', name: "Kozel",
degree: 11, degree: 11,
packaging: 'can' packaging: "can",
}; };
// Remove // Remove
export const del = yup.object({ export const del = yup.object({
_id: yup.string().required() _id: yup.string().required(),
}); });
export interface IDel extends yup.InferType<typeof del> {} export interface IDel extends yup.InferType<typeof del> {}
export const delExam: IDel = { export const delExam: IDel = {
_id: '6352b303b71cb62222f39895' _id: "6352b303b71cb62222f39895",
}; };
// Update // Update
@@ -41,12 +41,17 @@ export const edit = yup.object({
degree: yup.number(), degree: yup.number(),
packaging: yup.string(), packaging: yup.string(),
//imgs: yup.mixed().when('$imgs', (imgs, schema) => //imgs: yup.mixed().when('$imgs', (imgs, schema) =>
// Array.isArray(imgs) ? schema.array() : schema.string() // Array.isArray(imgs) ? schema.array() : schema.string()
//) //)
imgs: yup.mixed().test('is-array-or-string', 'imgs must be either an array or a string', value => imgs: yup
Array.isArray(value) || typeof value === 'string') .mixed()
.test(
"is-array-or-string",
"imgs must be either an array or a string",
(value) => Array.isArray(value) || typeof value === "string",
),
//imgs: yup.mixed().when('isArray', { //imgs: yup.mixed().when('isArray', {
// is: Array.isArray, // is: Array.isArray,
@@ -56,10 +61,10 @@ export const edit = yup.object({
}); });
export interface IEdit extends yup.InferType<typeof edit> {} export interface IEdit extends yup.InferType<typeof edit> {}
export const editExam: IEdit = { export const editExam: IEdit = {
_id: '6355b95dc03fad77bc380146', _id: "6355b95dc03fad77bc380146",
brand: 'Pilsner Urqell', brand: "Pilsner Urqell",
name: 'Radegast', name: "Radegast",
degree: 12, degree: 12,
packaging: 'bottle', packaging: "bottle",
imgs: [] imgs: [],
}; };

View File

@@ -0,0 +1,43 @@
import * as yup from "yup";
import mongoose, { Schema } from "mongoose";
interface mongooseAddition {
_id?: Schema.Types.ObjectId;
createdAt?: Schema.Types.Date;
updatedAt?: Schema.Types.Date;
}
let objectIdSchema = yup
.string()
.test("is-objectId", "Invalid ObjectId", (value: any) => mongoose.Types.ObjectId.isValid(value));
// Add
export const add = yup.object({
beer_id: objectIdSchema,
foam: yup.number().min(1).max(3).required(),
bitter_sweetness: yup.number().min(1).max(5).required(),
taste: yup.number().min(1).max(5).required(),
packaging: yup.number().min(1).max(5).required(),
sourness: yup.boolean().required(),
would_again: yup.boolean().required(),
user_id: yup.string().notRequired()
});
export interface IReview extends yup.InferType<typeof add>, mongooseAddition {}
export const addExam: IReview = {
beer_id: "6352b303b71cb62222f39895",
foam: 3,
bitter_sweetness: 2,
taste: 5,
packaging: 3,
sourness: false,
would_again: true,
};
// Remove
export const del = yup.object({
_id: objectIdSchema.required(),
});
export interface IDel extends yup.InferType<typeof del> {}
export const delExam: IDel = {
_id: "6352b303b71cb62222f39895",
};

View File

@@ -1,4 +1,4 @@
import { connectDB, dropDB, dropCollections } from '../src/utils/test_mongodb'; import { connectDB, dropDB, dropCollections } from "../src/utils/test_mongodb";
beforeAll(async () => { beforeAll(async () => {
await connectDB(); await connectDB();

View File

@@ -1,22 +1,22 @@
import supertest from 'supertest'; import supertest from "supertest";
import { app } from '../src/app'; import { app } from "../src/app";
import { connectDB, dropDB, dropCollections } from '../src/utils/test_mongodb'; import { connectDB, dropDB, dropCollections } from "../src/utils/test_mongodb";
const request = supertest(app); const request = supertest(app);
export const getJWT = async () => { export const getJWT = async () => {
try { try {
const resReg: any = await request.post('/api/v1/auth/signup').send({ const resReg: any = await request.post("/api/v1/auth/signup").send({
email: 'test@example.local', email: "test@example.local",
password: 'admin1234', password: "admin1234",
username: 'Test Test' username: "Test Test",
}); });
const resLog: any = await request.post('/api/auth/login').send({ const resLog: any = await request.post("/api/auth/login").send({
email: 'test@example.local', email: "test@example.local",
password: 'admin1234' password: "admin1234",
}); });
if (resLog.statusCode != 200) throw 'error while logging in'; if (resLog.statusCode != 200) throw "error while logging in";
const body = JSON.parse(resLog.text); const body = JSON.parse(resLog.text);
return Promise.resolve(body.data.jwt); return Promise.resolve(body.data.jwt);
@@ -31,194 +31,194 @@ export const getJWT = async () => {
* @returns JWT cookie * @returns JWT cookie
*/ */
export async function login(): Promise<string> { export async function login(): Promise<string> {
const res = await request.post('/api/v1/auth/signin').send({ const res = await request.post("/api/v1/auth/signin").send({
email: 'thisistest@host.local', email: "thisistest@host.local",
password: 'Admin1234' password: "Admin1234",
}); });
return res.headers['set-cookie']; return res.headers["set-cookie"];
} }
export async function signup(): Promise<boolean> { export async function signup(): Promise<boolean> {
const res = await request.post('/api/v1/auth/signup').send({ const res = await request.post("/api/v1/auth/signup").send({
email: 'thisistest@host.local', email: "thisistest@host.local",
password: 'Admin1234', password: "Admin1234",
username: 'Test Test' username: "Test Test",
}); });
if (res.statusCode == 201) return true; if (res.statusCode == 201) return true;
return false; return false;
} }
describe('POST /api/v1/auth/signup', () => { describe("POST /api/v1/auth/signup", () => {
describe('should drop validation error', () => { describe("should drop validation error", () => {
it('should drop 400 (empty request))', async () => { it("should drop 400 (empty request))", async () => {
const res: any = await request.post('/api/v1/auth/signup').send({}); const res: any = await request.post("/api/v1/auth/signup").send({});
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
}); });
it('should drop 400 (email))', async () => { it("should drop 400 (email))", async () => {
const res: any = await request.post('/api/v1/auth/signup').send({ const res: any = await request.post("/api/v1/auth/signup").send({
email: '', email: "",
username: 'User Admin', username: "User Admin",
password: 'Admin1234' password: "Admin1234",
}); });
console.log(res) console.log(res);
const body = JSON.parse(res.text); const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('email'); expect(body.data.path).toBe("email");
}); });
it('should drop 400 (username))', async () => { it("should drop 400 (username))", async () => {
const res: any = await request.post('/api/v1/auth/signup').send({ const res: any = await request.post("/api/v1/auth/signup").send({
email: 'admin@localhost.local', email: "admin@localhost.local",
username: '', username: "",
password: 'Admin1234' password: "Admin1234",
}); });
const body = JSON.parse(res.text); const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('username'); expect(body.data.path).toBe("username");
}); });
it('should drop 400 (password))', async () => { it("should drop 400 (password))", async () => {
const res: any = await request.post('/api/v1/auth/signup').send({ const res: any = await request.post("/api/v1/auth/signup").send({
email: 'admin@localhost.local', email: "admin@localhost.local",
username: 'User Admin', username: "User Admin",
password: '' password: "",
}); });
const body = JSON.parse(res.text); const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password'); expect(body.data.path).toBe("password");
}); });
it('should drop 400 (password - min 8 chars', async () => { it("should drop 400 (password - min 8 chars", async () => {
const res = await request.post('/api/v1/auth/signup').send({ const res = await request.post("/api/v1/auth/signup").send({
email: 'admin@localhost.local', email: "admin@localhost.local",
username: 'User Admin', username: "User Admin",
password: 'Admin12' password: "Admin12",
}); });
const body = JSON.parse(res.text); const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password'); expect(body.data.path).toBe("password");
}); });
it('should drop 400 (password - min 1 number', async () => { it("should drop 400 (password - min 1 number", async () => {
const res = await request.post('/api/v1/auth/signup').send({ const res = await request.post("/api/v1/auth/signup").send({
email: 'admin@localhost.local', email: "admin@localhost.local",
username: 'User Admin', username: "User Admin",
password: 'Adminadmin' password: "Adminadmin",
}); });
const body = JSON.parse(res.text); const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password'); expect(body.data.path).toBe("password");
}); });
it('should drop 400 (password - min 1 uppercase', async () => { it("should drop 400 (password - min 1 uppercase", async () => {
const res = await request.post('/api/v1/auth/signup').send({ const res = await request.post("/api/v1/auth/signup").send({
email: 'admin@localhost.local', email: "admin@localhost.local",
username: 'User Admin', username: "User Admin",
password: 'admin1234' password: "admin1234",
}); });
const body = JSON.parse(res.text); const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password'); expect(body.data.path).toBe("password");
}); });
}); });
test('should register an user', async () => { test("should register an user", async () => {
const res: any = await request.post('/api/v1/auth/signup').send({ const res: any = await request.post("/api/v1/auth/signup").send({
email: 'thisistest@host.local', email: "thisistest@host.local",
password: 'Admin1234', password: "Admin1234",
username: 'Test Test' username: "Test Test",
}); });
expect(res.statusCode).toBe(201); expect(res.statusCode).toBe(201);
}); });
}); });
describe('POST /api/v1/auth/signin', () => { describe("POST /api/v1/auth/signin", () => {
const url = '/api/v1/auth/signin'; const url = "/api/v1/auth/signin";
describe('should drop an validation error', () => { describe("should drop an validation error", () => {
it('should drop 400 (empty)', async () => { it("should drop 400 (empty)", async () => {
const res = await request.post(url).send(); const res = await request.post(url).send();
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
}); });
it('should drop 400 (email)', async () => { it("should drop 400 (email)", async () => {
const res = await request.post(url).send({ const res = await request.post(url).send({
password: 'Admin1234' password: "Admin1234",
}); });
const body = JSON.parse(res.text); const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('email'); expect(body.data.path).toBe("email");
}); });
it('should drop 400 (password)', async () => { it("should drop 400 (password)", async () => {
const res = await request.post(url).send({ const res = await request.post(url).send({
email: 'thisistest@host.local' email: "thisistest@host.local",
}); });
const body = JSON.parse(res.text); const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password'); expect(body.data.path).toBe("password");
}); });
}); });
test('should drop 401', async () => { test("should drop 401", async () => {
const res = await request.post(url).send({ const res = await request.post(url).send({
email: 'thisistest@host.local', email: "thisistest@host.local",
password: 'Test12365465132' password: "Test12365465132",
}); });
expect(res.statusCode).toBe(401); expect(res.statusCode).toBe(401);
expect(res.header['set-cookie'][0]).toContain("jwt=; Max-Age=0") expect(res.header["set-cookie"][0]).toContain("jwt=; Max-Age=0");
expect(res.header['set-cookie'][1]).toContain("auth=false") expect(res.header["set-cookie"][1]).toContain("auth=false");
}); });
test('should login an user', async () => { test("should login an user", async () => {
const res: any = await request.post(url).send({ const res: any = await request.post(url).send({
email: 'thisistest@host.local', email: "thisistest@host.local",
password: 'Admin1234' password: "Admin1234",
}); });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
expect(res.header['set-cookie'][0]).toContain("jwt=") expect(res.header["set-cookie"][0]).toContain("jwt=");
expect(res.header['set-cookie'][1]).toContain("auth=true") expect(res.header["set-cookie"][1]).toContain("auth=true");
}); });
}); });
describe('POST /api/v1/auth/logout', () => { describe("POST /api/v1/auth/logout", () => {
const url = '/api/v1/auth/logout'; const url = "/api/v1/auth/logout";
test('should drop 401 error', async () => { test("should drop 401 error", async () => {
const res = await request.post(url).send({}); const res = await request.post(url).send({});
expect(res.statusCode).toBe(401); expect(res.statusCode).toBe(401);
}); });
test('should logout an user', async () => { test("should logout an user", async () => {
const jwt = await login(); const jwt = await login();
const res = await request.post(url).set('Cookie', jwt).send(); const res = await request.post(url).set("Cookie", jwt).send();
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
expect(res.header['set-cookie'][0]).toContain("jwt=; Max-Age=0") expect(res.header["set-cookie"][0]).toContain("jwt=; Max-Age=0");
expect(res.header['set-cookie'][1]).toContain("auth=false") expect(res.header["set-cookie"][1]).toContain("auth=false");
}); });
}); });
describe('GET /api/v1/auth/status', () => { describe("GET /api/v1/auth/status", () => {
const url = '/api/v1/auth/status'; const url = "/api/v1/auth/status";
test('should return login status 401', async () => { test("should return login status 401", async () => {
const res = await request.get(url).send(); const res = await request.get(url).send();
expect(res.statusCode).toBe(401); expect(res.statusCode).toBe(401);
}); });
test('should return login status 200', async () => { test("should return login status 200", async () => {
const jwt = await login(); const jwt = await login();
const res = await request.get(url).set('Cookie', jwt).send(); const res = await request.get(url).set("Cookie", jwt).send();
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
expect(res.body.data.username).toBe("Test Test") expect(res.body.data.username).toBe("Test Test");
expect(res.body.data.email).toBe("thisistest@host.local") expect(res.body.data.email).toBe("thisistest@host.local");
expect(res.body.data.password).toBeUndefined() expect(res.body.data.password).toBeUndefined();
}); });
}); });

View File

@@ -1,166 +1,193 @@
import supertest from 'supertest'; import supertest from "supertest";
import { app } from '../src/app'; import { app } from "../src/app";
import { login } from './auth.test'; import { login } from "./auth.test";
import { addExam, delExam, editExam } from '../src/validators/beerValidator'; import { addExam, delExam, editExam } from "../src/validators/beerValidator";
const request = supertest(app); const request = supertest(app);
describe('POST /api/v1/beer/add', () => { describe("POST /api/v1/beer/add", () => {
const url = '/api/v1/beer/add'; const url = "/api/v1/beer/add";
test('should drop 401 error', async () => { test("should drop 401 error", async () => {
const res = await request.post(url).send({}); const res = await request.post(url).send({});
expect(res.statusCode).toBe(401); expect(res.statusCode).toBe(401);
}); });
test('should drop 400 ()', async () => { test("should drop 400 ()", async () => {
const jwt = await login(); const jwt = await login();
const res = await request.post(url).set('Cookie', jwt).send({}); const res = await request.post(url).set("Cookie", jwt).send({});
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
}); });
test('should drop 400 (brand)', async () => { test("should drop 400 (name)", async () => {
const jwt = await login();
const body: any = { ...addExam };
delete body.name;
const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400);
});
test("should drop 400 (degree)", async () => {
const jwt = await login();
const body: any = { ...addExam };
delete body.degree;
const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400);
});
test("should drop 400 (packaging)", async () => {
const jwt = await login();
const body: any = { ...addExam };
delete body.packaging;
const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400);
});
test("should drop 400 (brand)", async () => {
const jwt = await login(); const jwt = await login();
const body: any = { ...addExam }; const body: any = { ...addExam };
delete body.brand; delete body.brand;
const res = await request.post(url).set('Cookie', jwt).send(body); const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
}); });
test('should drop 201', async () => { test("should drop 201", async () => {
const jwt = await login(); const jwt = await login();
const res = await request.post(url).set('Cookie', jwt).send(addExam); const res = await request.post(url).set("Cookie", jwt).send(addExam);
expect(res.statusCode).toBe(201); expect(res.statusCode).toBe(201);
}); });
}); });
describe('GET /api/v1/beer/get', () => { describe("GET /api/v1/beer/get", () => {
const url = '/api/v1/beer/get'; const url = "/api/v1/beer/get";
test('should drop 401', async () => { test("should drop 401", async () => {
const res = await request.get(url).send(); const res = await request.get(url).send();
expect(res.statusCode).toBe(401); expect(res.statusCode).toBe(401);
}); });
test('should drop 200', async () => { test("should drop 200", async () => {
const jwt = await login(); const jwt = await login();
const res = await request.get(url).set('Cookie', jwt).send(); const res = await request.get(url).set("Cookie", jwt).send();
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
}); });
}); });
describe('POST /api/v1/beer/del', () => { describe("POST /api/v1/beer/del", () => {
const url = '/api/v1/beer/del'; const url = "/api/v1/beer/del";
test('should drop 401', async () => { test("should drop 401", async () => {
const res = await request.post(url).send(); const res = await request.post(url).send();
expect(res.statusCode).toBe(401); expect(res.statusCode).toBe(401);
}); });
test('should drop 400', async () => { test("should drop 400", async () => {
const jwt = await login(); const jwt = await login();
const res = await request.post(url).set('Cookie', jwt).send(delExam); const res = await request.post(url).set("Cookie", jwt).send(delExam);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
}); });
test('should drop 400', async () => { test("should drop 400", async () => {
const jwt = await login(); const jwt = await login();
const res = await request.post(url).set('Cookie', jwt).send({ const res = await request.post(url).set("Cookie", jwt).send({
_id: 'thisWillNotWork' _id: "thisWillNotWork",
}); });
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
}); });
test('should drop 200', async () => { test("should drop 200", async () => {
const jwt = await login(); const jwt = await login();
const req = await request.post('/api/v1/beer/add').set('Cookie', jwt).send(addExam); const req = await request.post("/api/v1/beer/add").set("Cookie", jwt).send(addExam);
const id = req.body.data._id; const id = req.body.data._id;
const res = await request.post(url).set('Cookie', jwt).send({ const res = await request.post(url).set("Cookie", jwt).send({
_id: id _id: id,
}); });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
}); });
}); });
describe('POST /api/v1/beer/edit', () => { describe("POST /api/v1/beer/edit", () => {
const url = '/api/v1/beer/edit'; const url = "/api/v1/beer/edit";
test('should drop 401', async () => { test("should drop 401", async () => {
const res = await request.post(url).send(); const res = await request.post(url).send();
expect(res.statusCode).toBe(401); expect(res.statusCode).toBe(401);
}); });
test('should drop 400', async () => { test("should drop 400", async () => {
const jwt = await login(); const jwt = await login();
const payload: any = { ...editExam }; const payload: any = { ...editExam };
delete payload._id; delete payload._id;
const res = await request.post(url).set('Cookie', jwt).send(payload); const res = await request.post(url).set("Cookie", jwt).send(payload);
expect(res.statusCode).toBe(400); expect(res.statusCode).toBe(400);
}); });
test('should drop 200', async () => { test("should drop 200", async () => {
const jwt = await login(); const jwt = await login();
const payload: any = { ...editExam }; const payload: any = { ...editExam };
delete payload.name; delete payload.name;
const res = await request.post(url).set('Cookie', jwt).send(payload); const res = await request.post(url).set("Cookie", jwt).send(payload);
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
}); });
test('should drop 200', async () => { test("should drop 200", async () => {
const jwt = await login(); const jwt = await login();
const payload: any = { ...editExam }; const payload: any = { ...editExam };
delete payload.degree; delete payload.degree;
const res = await request.post(url).set('Cookie', jwt).send(payload); const res = await request.post(url).set("Cookie", jwt).send(payload);
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
}); });
test('should drop 200', async () => { test("should drop 200", async () => {
const jwt = await login(); const jwt = await login();
const payload: any = { ...editExam }; const payload: any = { ...editExam };
delete payload.packaging; delete payload.packaging;
const res = await request.post(url).set('Cookie', jwt).send(payload); const res = await request.post(url).set("Cookie", jwt).send(payload);
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
}); });
test('should drop 200', async () => { test("should drop 200", async () => {
const jwt = await login(); const jwt = await login();
const payload: any = { ...editExam }; const payload: any = { ...editExam };
delete payload.brand; delete payload.brand;
const res = await request.post(url).set('Cookie', jwt).send(payload); const res = await request.post(url).set("Cookie", jwt).send(payload);
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
}); });
test('should drop 200', async () => { test("should drop 200", async () => {
const jwt = await login(); const jwt = await login();
const req = await request.post('/api/v1/beer/add').set('Cookie', jwt).send(addExam); const req = await request.post("/api/v1/beer/add").set("Cookie", jwt).send(addExam);
const _id = req.body.data._id; const _id = req.body.data._id;
const payload = { ...editExam, _id: _id }; const payload = { ...editExam, _id: _id };
let res = await request.post(url).set('Cookie', jwt).send(payload); let res = await request.post(url).set("Cookie", jwt).send(payload);
delete res.body.data._id; delete res.body.data._id;
delete res.body.data.__v; delete res.body.data.__v;
@@ -172,4 +199,4 @@ describe('POST /api/v1/beer/edit', () => {
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
expect(eq).toBe(true); expect(eq).toBe(true);
}); });
}); });

View File

@@ -1,12 +1,12 @@
import request from 'supertest'; import request from "supertest";
import { app } from '../src/app'; import { app } from "../src/app";
describe('GET /api/v1', () => { describe("GET /api/v1", () => {
describe('should return json with docs', () => { describe("should return json with docs", () => {
test('should respond with a 200 status code', async () => { test("should respond with a 200 status code", async () => {
const response = await request(app).get('/api/v1').send({}); const response = await request(app).get("/api/v1").send({});
expect(response.headers['content-type']).toMatch(/json/); expect(response.headers["content-type"]).toMatch(/json/);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
}); });
}); });
}); });

99
api/tests/review.test.ts Normal file
View File

@@ -0,0 +1,99 @@
import supertest from "supertest";
import { app } from "../src/app";
import { login } from "./auth.test";
import { addExam, delExam } from "../src/validators/reviewValidator";
const request = supertest(app);
describe("POST /api/v1/review/add", () => {
const url = "/api/v1/review/add";
test("should drop 401 error", async () => {
const res = await request.post(url).send({});
expect(res.statusCode).toBe(401);
});
test("should drop 400 ()", async () => {
const jwt = await login();
const res = await request.post(url).set("Cookie", jwt).send({});
expect(res.statusCode).toBe(400);
});
test("should drop 400 (foam)", async () => {
const jwt = await login();
const body: any = { ...addExam };
delete body.foam;
const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400);
});
test("should drop 400 (bitter_sweetness)", async () => {
const jwt = await login();
const body: any = { ...addExam };
delete body.bitter_sweetness;
const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400);
});
test("should drop 400 (taste)", async () => {
const jwt = await login();
const body: any = { ...addExam };
delete body.taste;
const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400);
});
test("should drop 400 (packaging)", async () => {
const jwt = await login();
const body: any = { ...addExam };
delete body.packaging;
const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400);
});
test("should drop 400 (sourness)", async () => {
const jwt = await login();
const body: any = { ...addExam };
delete body.sourness;
const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400);
});
test("should drop 400 (would_again)", async () => {
const jwt = await login();
const body: any = { ...addExam };
delete body.would_again;
const res = await request.post(url).set("Cookie", jwt).send(body);
expect(res.statusCode).toBe(400);
});
test("should drop 201", async () => {
const jwt = await login();
const res = await request.post(url).set("Cookie", jwt).send(addExam);
expect(res.statusCode).toBe(201);
});
});
describe("GET /api/v1/review/get", () => {
const url = "/api/v1/review/get";
test("should drop 401", async () => {
const res = await request.get(url).send();
expect(res.statusCode).toBe(401);
});
test("should drop 200", async () => {
const jwt = await login();
const res = await request.get(url).set("Cookie", jwt).send();
expect(res.statusCode).toBe(200);
});
});

1
frontend/.env.example Normal file
View File

@@ -0,0 +1 @@
EXPO_PUBLIC_API_URL=http://10.69.1.137:6060/api/v1

5
frontend/.gitignore vendored
View File

@@ -33,3 +33,8 @@ yarn-error.*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
.env
.vscode/
*.swp

View File

@@ -1,18 +0,0 @@
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { LoginForm } from "./screens/Login";
export default function App() {
return (
<View style={styles.container}>
<StatusBar style="light" />
<LoginForm />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -1,34 +1,41 @@
{ {
"expo": { "expo": {
"name": "deguapp", "name": "deguapp",
"slug": "deguapp", "slug": "deguapp",
"scheme": "deguapp", "scheme": "deguapp",
"version": "1.0.0", "version": "1.0.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"userInterfaceStyle": "light", "userInterfaceStyle": "light",
"splash": { "splash": {
"image": "./assets/splash.png", "image": "./assets/splash.png",
"resizeMode": "contain", "resizeMode": "contain",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"assetBundlePatterns": [ "assetBundlePatterns": ["**/*"],
"**/*" "ios": {
], "supportsTablet": true
"ios": { },
"supportsTablet": true "android": {
}, "adaptiveIcon": {
"android": { "foregroundImage": "./assets/adaptive-icon.png",
"adaptiveIcon": { "backgroundColor": "#ffffff"
"foregroundImage": "./assets/adaptive-icon.png", }
"backgroundColor": "#ffffff" },
} "web": {
}, "favicon": "./assets/favicon.png"
"web": { },
"favicon": "./assets/favicon.png" "plugins": [
}, "expo-router",
"plugins": [ [
"expo-router" "expo-image-picker",
] {
} "photosPermission": "The app accesses your photos to let you share them with your friends.",
"cameraPermission": "The app accesses your camera to let you take photos of your beer and share them with your friends.",
"microphonePermission": "The app accesses your microphone to let you record audio and share it with your friends."
}
],
"expo-secure-store"
]
}
} }

View File

@@ -0,0 +1,76 @@
import React from "react";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import { Tabs } from "expo-router";
import { StyleSheet, View } from "react-native";
import { colors } from "@components/style";
import { StatusBar } from "expo-status-bar";
export default function TabLayout() {
return (
<View style={{ flex: 1 }}>
<StatusBar style="light" />
<Tabs
screenOptions={{
headerStyle: {
backgroundColor: colors.dark,
},
headerTintColor: "white",
tabBarStyle: {
backgroundColor: colors.darkSecondary,
},
tabBarActiveTintColor: colors.gold,
headerShown: true,
}}
sceneContainerStyle={{ backgroundColor: colors.dark }}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => (
<FontAwesome size={28} name="home" color={color} />
),
}}
/>
<Tabs.Screen
name="beer/index"
options={{
title: "Beers",
tabBarIcon: ({ color }) => (
<FontAwesome size={28} name="beer" color={color} />
),
}}
/>
<Tabs.Screen
name="review/index"
options={{
title: "Reviews",
tabBarIcon: ({ color }) => (
<MaterialIcons size={28} name="reviews" color={color} />
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
tabBarIcon: ({ color }) => (
<FontAwesome size={28} name="cog" color={color} />
),
}}
/>
{/* Hide following routes from bottom bar */}
<Tabs.Screen
name="beer/add"
options={{ href: null, title: "Add beer" }}
/>
<Tabs.Screen
name="review/add/[beer_id]"
options={{ href: null, title: "Add review" }}
/>
</Tabs>
</View>
);
}

View File

@@ -0,0 +1,263 @@
import { StyleSheet, TextInput, View, Image } from "react-native";
import { useState } from "react";
import Button from "@components/Button";
import Text from "@components/Text";
import { colors } from "@components/style";
import * as ImagePicker from "expo-image-picker";
import DropDownPicker from "react-native-dropdown-picker";
const DropdownTheme = require("@components/DropdownTheme");
import { Platform } from "react-native";
export default function BeerAdd() {
const [b_name, setBName] = useState("");
const [b_degree, setBDegree] = useState("");
const [b_packaging, setBPackaging] = useState(null);
const [b_brand, setBBrand] = useState("");
const [image, setImage] = useState(null);
const [open, setOpen] = useState(false);
const [items, setItems] = useState([
{ label: "Tank beer", value: "tank" },
{ label: "Cask beer", value: "cask" },
{ label: "Glass bottle", value: "glass" },
{ label: "Can", value: "can" },
{ label: "PET bottle", value: "pet" },
]);
DropDownPicker.addTheme("DropdownTheme", DropdownTheme);
DropDownPicker.setTheme("DropdownTheme");
ImagePicker.getCameraPermissionsAsync(); //check if the user has granted permission to access the camera
const pickImage = async () => {
const permissionResult =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permissionResult.granted === false) {
alert("You've refused to allow this appp to access your photos!");
return;
}
// No permissions request is necessary for launching the image library
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [3, 4],
// quality: 1,
});
// Explore the result
console.log(result);
if (!result.canceled) {
setImage(result.assets[0]);
}
};
const openCamera = async () => {
// Ask the user for the permission to access the camera
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
if (permissionResult.granted === false) {
alert("You've refused to allow this app to access your camera!");
return;
}
const result = await ImagePicker.launchCameraAsync();
// Explore the result
console.log(result);
if (!result.canceled) {
setImage(result.assets[0]);
}
};
function validateDegreeInput(text) {
let newText = "";
let numbers = "0123456789.";
for (var i = 0; i < text.length; i++) {
if (numbers.indexOf(text[i]) > -1) {
newText = newText + text[i];
setBDegree(newText);
} else {
// your call back function
alert("Please enter numbers only.");
setBDegree("");
}
}
}
function dataURItoBlob(dataURI) {
// convert base64/URLEncoded data component to raw binary data held in a string
var byteString;
if (dataURI.split(",")[0].indexOf("base64") >= 0)
byteString = atob(dataURI.split(",")[1]);
else byteString = unescape(dataURI.split(",")[1]);
// separate out the mime component
var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
// write the bytes of the string to a typed array
var ia = new Uint8Array(byteString.length);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ia], { type: mimeString });
}
async function addBeer() {
// TODO: after the request - redirect to /beer/{new_beer_id}?; plus some modal about successful state
const data = new FormData();
if (Platform.OS == "web") {
// TODO: On phone its imposibble to upload an image
data.append("photos", dataURItoBlob(image.uri));
}
data.append("brand", b_brand);
data.append("name", b_name);
data.append("degree", b_degree);
data.append("packaging", "can");
try {
const req = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/beer/add`, {
method: "POST",
credentials: "include",
body: data,
});
const res = await req.json();
if (res.code == 201 && res.data._id) {
// TODO: reditect using expo router
// window.location.href = `/beer/${res.data._id}`;
alert("Added");
} else {
alert(
"Beer was not added successfully. Please check your data and try again.",
);
}
} catch (err) {
alert(
"Beer was not added successfully. Please check your data and try again.",
);
console.error(err);
}
}
return (
<View style={styles.container}>
<View style={styles.form}>
<Text style={styles.text}>
Spill your thoughts about the beer you just sipped!
</Text>
<TextInput
style={styles.input}
placeholder="Name"
value={b_name}
onChangeText={(text) => setBName(text)}
placeholderTextColor="#aaaaaa"
/>
<TextInput
style={styles.input}
placeholder="Brand"
value={b_brand}
onChangeText={(text) => setBBrand(text)}
placeholderTextColor="#aaaaaa"
/>
<TextInput
style={styles.input}
placeholder="Degree"
value={b_degree}
onChangeText={(text) => validateDegreeInput(text)}
placeholderTextColor="#aaaaaa"
keyboardType="numeric"
maxLength={3}
/>
<DropDownPicker
open={open}
value={b_packaging}
items={items}
setOpen={setOpen}
setValue={setBPackaging}
setItems={setItems}
placeholder={"What are you drinking from?"}
theme="DropdownTheme"
//searchable={true} //maybe we can use it later...
/>
<View style={styles.imageContainer}>
<Button
title="Open gallery"
onPress={pickImage}
buttonStyle={styles.imageButton}
textStyle={styles.imageTextButton}
/>
{Platform.OS != "web" ? (
<Button
title="Open camera"
onPress={openCamera}
buttonStyle={styles.imageButton}
textStyle={styles.imageTextButton}
/>
) : (
false
)}
</View>
{image && <Image source={{ uri: image.uri }} style={styles.image} />}
<Button title="Add beer" color={colors.gold} onPress={addBeer} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
width: "100%",
height: "100%",
alignItems: "center",
display: "flex",
},
form: {
alignItems: "center",
gap: 15,
width: "80%",
},
input: {
height: "auto",
width: "100%",
borderColor: "gray",
borderWidth: 1,
borderRadius: 10,
padding: 13,
color: "#fff",
},
imageContainer: {
alignItems: "center",
justifyContent: "center",
display: "flex",
flexDirection: "row",
gap: 10,
},
imageButton: {
backgroundColor: colors.dark,
borderColor: "gray",
borderWidth: 1,
borderRadius: 10,
},
imageTextButton: {
color: colors.white,
},
image: {
width: 150,
height: 150,
},
text: {
color: colors.white,
fontSize: 24,
textAlign: "center",
paddingBottom: "3%",
paddingTop: "10%",
},
});

View File

@@ -0,0 +1,128 @@
import {
StyleSheet,
View,
FlatList,
Dimensions,
StatusBar,
Image,
} from "react-native";
import Text from "@components/Text";
import Button from "@components/Button";
import { colors } from "@components/style";
import { router } from "expo-router";
import { useEffect, useState } from "react";
// import { FlashList } from "@shopify/flash-list";
export default function Tab() {
const API_HOST = process.env.EXPO_PUBLIC_API_URL.replace("/api/v1", "");
const [data, setData] = useState([]);
useEffect(() => {
fetchData();
}, []);
async function fetchData() {
try {
const res = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/beer/get`, {
method: "GET",
credentials: "include",
});
const data = await res.json();
setData(data.data);
} catch (err) {
console.error(err);
alert("Something went wrong");
}
}
return (
<View style={styles.container}>
<Button
title="Add new beer"
color={colors.gold}
onPress={() => {
router.replace("/beer/add");
}}
/>
{/* <FlashList
data={data}
estimatedItemSize={100}
keyExtractor={(item) => String(item._id)}
renderItem={({ item }) => (
<View style={styles.item}>
<Text>Name: {item.name}</Text>
<Text>Brand: {item.brand}</Text>
<Text>Degree: {item.degree}</Text>
<Text>Packaging: {item.packaging}</Text>
</View>
)}
/> */}
<FlatList
data={data}
style={styles.beerList}
keyExtractor={(item) => String(item._id)}
renderItem={({ item }) => (
<View style={styles.item}>
<Image
source={
item.imgs[0]
? {
uri: `${API_HOST}/public/uploads/${item.imgs[0]}`,
}
: {
uri: "https://imagesvc.meredithcorp.io/v3/mm/image?url=https:%2F%2Fstatic.onecms.io%2Fwp-content%2Fuploads%2Fsites%2F44%2F2020%2F09%2F29%2Flight-beer.jpg",
}
}
style={styles.itemImg}
/>
<View style={styles.itemDesc}>
<Text>Name: {item.name}</Text>
<Text>Brand: {item.brand}</Text>
<Text>Degree: {item.degree}</Text>
<Text>Packaging: {item.packaging}</Text>
</View>
<View style={styles.itemAddReview}>
<Button
title="Add review"
color={colors.gold}
onPress={() => {
router.push(`/review/add/${item._id}`);
}}
/>
</View>
</View>
)}
/>
</View>
);
}
export const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
marginTop: "2%",
},
beerList: {
width: "100%",
paddingHorizontal: "15%",
marginTop: "2%",
},
item: {
borderColor: "gray",
borderWidth: 1,
borderRadius: 10,
padding: 13,
marginBottom: "5%",
},
itemImg: {
height: 300,
resizeMode: "contain",
},
itemDesc: {
alignItems: "center",
paddingBottom: "2%",
},
});

View File

@@ -0,0 +1,53 @@
import { StyleSheet, Text, View } from "react-native";
import Button from "@components/Button";
import { colors } from "@components/style";
import { useAuth } from "@context/AuthContext";
import { router } from "expo-router";
export default function Index() {
const { onLogout, authState } = useAuth();
const user = authState.user;
return (
<View style={styles.container}>
<Text style={styles.h1}>Welcome {user.username}!</Text>
<Text style={styles.h2}>We hope, you're enjoying your beer.</Text>
<View style={styles.button}>
<Button
style={styles.button}
title="Add beer!"
color={colors.gold}
textColor={colors.white}
onPress={() => {
router.push("/beer");
}}
></Button>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
},
h1: {
color: "#FFF",
fontSize: 30,
textAlign: "center",
paddingTop: "20%",
},
h2: {
color: "#FFF",
fontSize: 20,
textAlign: "center",
paddingTop: "1%",
},
button: {
color: colors.charcoal,
fontSize: 30,
textAlign: "center",
paddingTop: "2%",
},
});

View File

@@ -0,0 +1,521 @@
import { StyleSheet, TextInput, View, Image } from "react-native";
import { useCallback, useState } from "react";
import Button from "@components/Button";
import Text from "@components/Text";
import { colors } from "@components/style";
import * as ImagePicker from "expo-image-picker";
import DropDownPicker from "react-native-dropdown-picker";
const DropdownTheme = require("@components/DropdownTheme");
import { Platform } from "react-native";
import { useLocalSearchParams } from "expo-router";
export default function reviewAdd() {
// States for each dropdown
const routeParams = useLocalSearchParams();
const [openFoam, setOpenFoam] = useState(false);
const [openBitterSweetness, setOpenBitterSweetness] = useState(false);
const [openTaste, setOpenTaste] = useState(false);
const [openPackaging, setOpenPackaging] = useState(false);
const [openSourness, setOpenSourness] = useState(false);
const [openAgain, setOpenAgain] = useState(false);
// pěna
const [itemFoam, setFoamValue] = useState(null);
const [foam, setFoam] = useState([
{
label: "Bad",
value: "1",
icon: () => (
<Image
source={require("@assets/smileys/smiley-x-eyes.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Medium",
value: "2",
icon: () => (
<Image
source={require("@assets/smileys/smiley-meh.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Excellent",
value: "3",
icon: () => (
<Image
source={require("@assets/smileys/smiley.png")}
style={styles.iconStyle}
/>
),
},
]);
// hořkost / sladkost
const [itemBitter_sweetness, setBitter_sweetnessValue] = useState(null);
const [bitter_sweetness, setBitter_sweetness] = useState([
{
label: "Bad",
value: "1",
icon: () => (
<Image
source={require("@assets/smileys/smiley-x-eyes.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Medium",
value: "2",
icon: () => (
<Image
source={require("@assets/smileys/smiley-meh.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Excellent",
value: "3",
icon: () => (
<Image
source={require("@assets/smileys/smiley.png")}
style={styles.iconStyle}
/>
),
},
]);
//chuť
const [itemTaste, setTasteValue] = useState(null);
const [taste, setTaste] = useState([
{
label: "Disgust",
value: "1",
icon: () => (
<Image
source={require("@assets/smileys/smiley-blank.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Not great, not terrible",
value: "2",
icon: () => (
<Image
source={require("@assets/smileys/smiley-nervous.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Good",
value: "3",
icon: () => (
<Image
source={require("@assets/smileys/smiley-meh.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Why not",
value: "4",
icon: () => (
<Image
source={require("@assets/smileys/smiley-wink.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Excellent!",
value: "5",
icon: () => (
<Image
source={require("@assets/smileys/smiley.png")}
style={styles.iconStyle}
/>
),
},
]);
// packaging
const [itemPackaging, setPackagingValue] = useState(null);
const [packaging, setPackaging] = useState([
{
label: "Disgust",
value: "1",
icon: () => (
<Image
source={require("@assets/smileys/smiley-blank.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Not great, not terrible",
value: "2",
icon: () => (
<Image
source={require("@assets/smileys/smiley-nervous.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Good",
value: "3",
icon: () => (
<Image
source={require("@assets/smileys/smiley-meh.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Why not",
value: "4",
icon: () => (
<Image
source={require("@assets/smileys/smiley-wink.png")}
style={styles.iconStyle}
/>
),
},
{
label: "Excellent!",
value: "5",
icon: () => (
<Image
source={require("@assets/smileys/smiley.png")}
style={styles.iconStyle}
/>
),
},
]);
//kyselost
const [itemSourness, setSournessValue] = useState(null);
const [sourness, setSourness] = useState([
{
label: "True",
value: true,
icon: () => (
<Image
source={require("@assets/smileys/smiley-blank.png")}
style={styles.iconStyle}
/>
),
},
{
label: "False",
value: false,
icon: () => (
<Image
source={require("@assets/smileys/smiley-nervous.png")}
style={styles.iconStyle}
/>
),
},
]);
//dal bych si znovu?
const [itemAgain, setAgainValue] = useState(null);
const [again, setAgain] = useState([
{
label: "Yes",
value: true,
icon: () => (
<Image
source={require("@assets/smileys/smiley.png")}
style={styles.iconStyle}
/>
),
},
{
label: "No",
value: false,
icon: () => (
<Image
source={require("@assets/smileys/smiley-x-eyes.png")}
style={styles.iconStyle}
/>
),
},
]);
//podmínky pro zavření ostatních dropdownů, pokud je jiný otevřený
const onOpenFoam = useCallback(() => {
setOpenBitterSweetness(false);
setOpenTaste(false);
setOpenPackaging(false);
setOpenSourness(false);
setOpenAgain(false);
setOpenFoam(true);
}, []);
const onOpenBitterSweetness = useCallback(() => {
setOpenFoam(false);
setOpenTaste(false);
setOpenPackaging(false);
setOpenSourness(false);
setOpenAgain(false);
setOpenBitterSweetness(true);
}, []);
const onOpenTaste = useCallback(() => {
setOpenFoam(false);
setOpenBitterSweetness(false);
setOpenPackaging(false);
setOpenSourness(false);
setOpenAgain(false);
setOpenTaste(true);
}, []);
const onOpenPackaging = useCallback(() => {
setOpenFoam(false);
setOpenBitterSweetness(false);
setOpenTaste(false);
setOpenSourness(false);
setOpenAgain(false);
setOpenPackaging(true);
}, []);
const onOpenSourness = useCallback(() => {
setOpenFoam(false);
setOpenBitterSweetness(false);
setOpenTaste(false);
setOpenPackaging(false);
setOpenAgain(false);
setOpenSourness(true);
}, []);
const onOpenAgain = useCallback(() => {
setOpenFoam(false);
setOpenBitterSweetness(false);
setOpenTaste(false);
setOpenPackaging(false);
setOpenSourness(false);
setOpenAgain(true);
}, []);
DropDownPicker.addTheme("DropdownTheme", DropdownTheme);
DropDownPicker.setTheme("DropdownTheme");
async function addBeer() {
const req = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/review/add`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
beer_id: routeParams.beer_id,
foam: itemFoam,
bitter_sweetness: itemBitter_sweetness,
taste: itemTaste,
packaging: itemPackaging,
sourness: itemSourness,
would_again: itemAgain,
}),
});
const res = await req.json();
if (res.code == 201 && res.data._id) {
// window.location.href = `/review/${res.data._id}`;
// TODO: use react router for redirect
alert("Review was added!");
} else {
alert(
"Review was not added successfully. Please check your data and try again.",
);
}
}
return (
<View style={styles.container}>
<View style={styles.form}>
<Text style={styles.text}>
How does your beer taste? Write a review!
</Text>
<Text style={styles.dropdownText} zIndex={6000} zIndexInverse={1000}>
How does the foam look like?
</Text>
<DropDownPicker
open={openFoam}
onOpen={onOpenFoam}
value={itemFoam}
items={foam}
setOpen={setOpenFoam}
setValue={setFoamValue}
setItems={setFoam}
placeholder="Please select..."
theme="DropdownTheme"
zIndex={6000}
zIndexInverse={1000}
/>
<Text style={styles.dropdownText} zIndex={5000} zIndexInverse={2000}>
More bitter, or more sweet?
</Text>
<DropDownPicker
open={openBitterSweetness}
onOpen={onOpenBitterSweetness}
value={itemBitter_sweetness}
items={bitter_sweetness}
setOpen={setOpenBitterSweetness}
setValue={setBitter_sweetnessValue}
setItems={setBitter_sweetness}
placeholder="Please select..."
theme="DropdownTheme"
zIndex={5000}
zIndexInverse={2000}
/>
<Text style={styles.dropdownText} zIndex={4000} zIndexInverse={3000}>
How does it taste?
</Text>
<DropDownPicker
open={openTaste}
onOpen={onOpenTaste}
value={itemTaste}
items={taste}
setOpen={setOpenTaste}
setValue={setTasteValue}
setItems={setTaste}
placeholder="Please select..."
theme="DropdownTheme"
zIndex={4000}
zIndexInverse={3000}
/>
<Text style={styles.dropdownText} zIndex={5000} zIndexInverse={4000}>
How do you like the packaging?
</Text>
<DropDownPicker
open={openPackaging}
onOpen={onOpenPackaging}
value={itemPackaging}
items={packaging}
setOpen={setOpenPackaging}
setValue={setPackagingValue}
setItems={setPackaging}
placeholder="Please select..."
theme="DropdownTheme"
zIndex={3000}
zIndexInverse={4000}
/>
<Text style={styles.dropdownText} zIndex={4000} zIndexInverse={5000}>
Is it sour?
</Text>
<DropDownPicker
open={openSourness}
onOpen={onOpenSourness}
value={itemSourness}
items={sourness}
setOpen={setOpenSourness}
setValue={setSournessValue}
setItems={setSourness}
placeholder="Please select..."
theme="DropdownTheme"
zIndex={2000}
zIndexInverse={5000}
/>
<Text style={styles.dropdownText} zIndex={5000} zIndexInverse={6000}>
Would you drink it again?
</Text>
<DropDownPicker
open={openAgain}
onOpen={onOpenAgain}
value={itemAgain}
items={again}
setOpen={setOpenAgain}
setValue={setAgainValue}
setItems={setAgain}
placeholder="Please select..."
theme="DropdownTheme"
zIndex={1000}
zIndexInverse={6000}
/>
<View style={styles.buttonSend}>
<Button title="Add review" color={colors.gold} onPress={addBeer} />
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
width: "100%",
height: "100%",
alignItems: "center",
display: "flex",
},
form: {
gap: 5,
width: "80%",
},
buttonSend: {
display: "flex",
alignItems: "center",
marginTop: "2%",
},
input: {
height: "auto",
width: "100%",
borderColor: "gray",
borderWidth: 1,
borderRadius: 10,
padding: 13,
color: "#fff",
},
imageContainer: {
alignItems: "center",
justifyContent: "center",
display: "flex",
flexDirection: "row",
gap: 10,
},
imageButton: {
backgroundColor: colors.dark,
borderColor: "gray",
borderWidth: 1,
borderRadius: 10,
},
imageTextButton: {
color: colors.white,
},
image: {
width: 150,
height: 150,
},
text: {
color: colors.white,
fontSize: 24,
textAlign: "center",
paddingBottom: "3%",
paddingTop: "10%",
},
iconStyle: {
width: 30,
height: 30,
},
dropdownContainer: {
width: "100%",
},
dropdownText: {
color: colors.white,
fontSize: 16,
paddingBottom: 1,
paddingTop: "1%",
display: "flex",
alignItems: "flex-start",
flexDirection: "column",
},
});

View File

@@ -0,0 +1,139 @@
import { View, StyleSheet, FlatList, Image } from "react-native";
import Text from "@components/Text";
import Button from "@components/Button";
import { colors } from "@components/style";
import { router } from "expo-router";
import { useEffect, useState } from "react";
import { useAuth } from "@context/AuthContext";
export default function Tab() {
const { authState } = useAuth();
const user = authState.user;
const [data, setData] = useState([]);
useEffect(() => {
fetchData();
}, []);
const API_HOST = process.env.EXPO_PUBLIC_API_URL.replace("/api/v1", "");
async function fetchData() {
try {
const res = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/review/get`, {
method: "GET",
credentials: "include",
});
let data = await res.json();
// show only logged in user's data
data = data.data.filter((review) => review.user_id == user._id);
let beers = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/beer/get`, {
method: "GET",
credentials: "include",
});
beers = await beers.json();
beers = beers.data;
console.log(beers);
async function getBeerParam(search, beers) {
for (let i = 0; i < beers.length; i++) {
if (beers[i]._id == search) {
return beers[i];
}
}
return null;
}
data.forEach(async (el) => {
el.beer = await getBeerParam(el.beer_id, beers);
});
console.log("reviews", data);
setData(data);
} catch (err) {
console.error(err);
alert("Something went wrong");
}
}
const opt3 = ["Bad", "Medium", "Excellent!"];
const opt5 = [
"Disgust",
"Not great, not terrible",
"Good",
"Why not?",
"Excellent!",
];
const opt2 = ["Yes", "No"];
const sourness = ["Good", "Bad"];
return (
<View style={styles.container}>
<FlatList
data={data}
style={styles.reviewList}
keyExtractor={(item) => String(item._id)}
renderItem={({ item }) => (
<View style={styles.itemContainer}>
<View>
<Text>{item.beer.name}</Text>
<Text>{item.beer.brand}</Text>
<Text>{item.beer.degree}°</Text>
<Text>{item.beer.packaging}</Text>
<Image
source={
item.beer.imgs[0]
? {
uri: `${API_HOST}/public/uploads/${item.beer.imgs[0]}`,
}
: {
uri: "https://imagesvc.meredithcorp.io/v3/mm/image?url=https:%2F%2Fstatic.onecms.io%2Fwp-content%2Fuploads%2Fsites%2F44%2F2020%2F09%2F29%2Flight-beer.jpg",
}
}
style={styles.itemImg}
/>
</View>
<View>
<Text>Foam {opt3[item.foam - 1]}</Text>
<Text>
Bitter / Sweetness {opt3[item.bitter_sweetness - 1]}
</Text>
<Text>Taste {opt5[item.taste - 1]}</Text>
<Text>Packaging {opt5[item.packaging - 1]}</Text>
<Text>Sourness {sourness[item.sourness - 1]}</Text>
<Text>Would again? {opt2[item.would_again - 1]}</Text>
</View>
</View>
)}
/>
</View>
);
}
export const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
marginTop: "5%",
},
reviewList: {
width: "100%",
paddingHorizontal: "15%",
marginTop: "5%",
},
itemContainer: {
borderColor: "gray",
borderWidth: 1,
borderRadius: 10,
padding: 13,
marginBottom: "5%",
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
},
itemImg: {
width: 150,
aspectRatio: 1,
resizeMode: "contain",
marginTop: "5%",
},
});

View File

@@ -0,0 +1,17 @@
import { View } from "react-native";
import Button from "@components/Button";
import { colors } from "@components/style";
import { useAuth } from "@context/AuthContext";
import Text from "@components/Text";
export default function Tab() {
const { onLogout, authState } = useAuth();
const user = authState.user;
return (
<View style={{ justifyContent: "center", alignItems: "center", flex: 1 }}>
<Text>Welcome {user.username}</Text>
<Button title="Log out" color={colors.brown} onPress={onLogout} />
</View>
);
}

View File

@@ -1,22 +1,27 @@
import { Redirect, Stack } from "expo-router"; import { Redirect, Stack, Slot } from "expo-router";
import { useAuth } from "@context/AuthContext";
import { useAuth } from "../context/AuthContext"; import { View, Text, StyleSheet } from "react-native";
import { View, Text } from "react-native";
export default function AppLayout() { export default function AppLayout() {
const { authState } = useAuth(); const { authState } = useAuth();
if (authState.authenticated === null) { if (authState.authenticated === null) {
// micro loading co neni skoro videt ale get the fuck out se uz neloguje // micro loading co neni skoro videt ale get the fuck out se uz neloguje
return ( return (
<View> <View>
<Text>Loading...</Text> <Text>Loading...</Text>
</View> </View>
); );
} }
if (!authState.authenticated) {
console.log("get the fuck out"); if (!authState.authenticated) {
return <Redirect href="/login" />; console.log("get the fuck out");
} return <Redirect href="/login" />;
return <Stack />; }
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
);
} }

View File

@@ -1,21 +0,0 @@
import { Text, View } from "react-native";
import { useAuth } from "../context/AuthContext";
export default function Index() {
const { onLogout } = useAuth();
const user = "debil"
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Welcome {user}</Text>
<Text
onPress={() => {
// The `app/(app)/_layout.tsx` will redirect to the sign-in screen.
onLogout();
}}
>
Sign Out
</Text>
</View>
);
}

View File

@@ -1,10 +1,12 @@
import { Slot } from "expo-router"; import { Slot } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { AuthProvider } from "./context/AuthContext"; import { AuthProvider } from "./context/AuthContext";
export default function Root() { export default function Root() {
return ( return (
<AuthProvider> <AuthProvider>
<Slot /> <StatusBar style="light" />
</AuthProvider> <Slot />
); </AuthProvider>
);
} }

View File

@@ -1,103 +1,139 @@
import { createContext, useContext, useEffect, useState } from "react"; import { createContext, useContext, useEffect, useState } from "react";
import axios from "axios";
import storageUtil from "./storage"; import storageUtil from "./storage";
const TOKEN_KEY = "my-jwt"; const TOKEN_KEY = "my-jwt";
export const API_URL = "http://10.69.1.137:6060/api/v1"; export const API_URL = process.env.EXPO_PUBLIC_API_URL;
const AuthContext = createContext(null); const AuthContext = createContext(null);
export function useAuth() { export function useAuth() {
const authContext = useContext(AuthContext); const authContext = useContext(AuthContext);
if (authContext === undefined) { if (authContext === undefined) {
throw new Error("Context is outside of provider"); throw new Error("Context is outside of provider");
} }
return authContext; return authContext;
} }
export function AuthProvider({ children }) { export function AuthProvider({ children }) {
const [authState, setAuthState] = useState({ const [authState, setAuthState] = useState({
token: null, token: null,
authenticated: null, authenticated: null,
}); });
useEffect(() => { useEffect(() => {
// tohle se zavola jen poprve pri startu appky // tohle se zavola jen poprve pri startu appky
async function loadToken() { async function loadToken() {
const token = await storageUtil.getItem(TOKEN_KEY); const token = await storageUtil.getItem(TOKEN_KEY);
console.log(`stored: ${token}`); console.log(`stored: ${token}`);
if (token) { const resUser = await fetch(`${API_URL}/auth/status`, {
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; credentials: "include",
});
setAuthState({ const userData = await resUser.json();
token: token,
authenticated: true,
});
return; if (token && resUser.status == 200) {
} setAuthState({
setAuthState({ token: token,
authenticated: false, authenticated: true,
token: null, user: userData.data,
}); });
}
loadToken();
}, []);
async function register(username, email, password) { return;
try { }
const res = await axios.post(`${API_URL}/auth/signup`, { setAuthState({
username, authenticated: false,
email, token: null,
password, user: null,
}); });
return res }
} catch (err) { loadToken();
return { error: true, msg: err.response.data}; }, []);
}
}
async function login(email, password) { async function register(username, email, password) {
try { try {
const res = await axios.post(`${API_URL}/auth/signin`, { const res = await fetch(`${API_URL}/auth/signup`, {
email, method: "POST",
password, credentials: "include",
}); headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
email,
password,
}),
});
return res;
} catch (err) {
return { error: true, msg: err.response.data };
}
}
setAuthState({ async function login(email, password) {
token: res.data.data.jwt, try {
authenticated: true, const resLogin = await fetch(`${API_URL}/auth/signin`, {
}); method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
});
//axios.defaults.headers.common[ const loginData = await resLogin.json();
// "Authorization"
//] = `Bearer ${res.data.data.jwt}`;
await storageUtil.setItem(TOKEN_KEY, res.data.data.jwt); const resUser = await fetch(`${API_URL}/auth/status`, {
credentials: "include",
});
return res if (resUser.status != 200) {
} catch (err) { throw Error("user does not have user data");
return { error: true, msg: err.res }; }
}
}
async function logout() { const userData = await resUser.json();
await storageUtil.delItem(TOKEN_KEY);
axios.defaults.headers.common["Authorization"] = ""; setAuthState({
token: loginData.data.jwt,
authenticated: true,
user: userData.data,
});
setAuthState({ await storageUtil.setItem(TOKEN_KEY, loginData.data.jwt);
token: null, } catch (err) {
authenticated: false, console.error("Failed to log in", err);
}); return { error: true, msg: err.res };
} }
}
const value = { async function logout() {
onSignin: register, let res = await fetch(`${API_URL}/auth/logout`, {
onLogin: login, method: "POST",
onLogout: logout, credentials: "include",
authState, headers: {
}; "Content-Type": "application/json",
},
body: JSON.stringify({}),
});
res = await res.json();
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; await storageUtil.delItem(TOKEN_KEY);
setAuthState({
token: null,
authenticated: false,
user: null,
});
}
const value = {
onSignin: register,
onLogin: login,
onLogout: logout,
authState,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
} }

View File

@@ -3,32 +3,32 @@ import { Platform } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
const storageUtil = { const storageUtil = {
setItem: async (k, v) => { setItem: async (k, v) => {
if (Platform.OS === "web") { if (Platform.OS === "web") {
// web // web
await AsyncStorage.setItem(k, v); await AsyncStorage.setItem(k, v);
} else { } else {
// mobile // mobile
await SecureStore.setItemAsync(k, v.toString()); // v must be string, await SecureStore.setItemAsync(k, v.toString()); // v must be string,
} }
}, },
getItem: async (k) => { getItem: async (k) => {
if (Platform.OS === "web") { if (Platform.OS === "web") {
// web // web
return await AsyncStorage.getItem(k); return await AsyncStorage.getItem(k);
} else { } else {
// mobile // mobile
return await SecureStore.getItemAsync(k); return await SecureStore.getItemAsync(k);
} }
}, },
delItem: async (k) => { delItem: async (k) => {
if (Platform.OS === "web") { if (Platform.OS === "web") {
// web // web
await AsyncStorage.removeItem(k); await AsyncStorage.removeItem(k);
} else { } else {
// mobile // mobile
await SecureStore.deleteItemAsync(k); await SecureStore.deleteItemAsync(k);
} }
}, },
}; };
export default storageUtil; export default storageUtil;

View File

@@ -1,6 +1,6 @@
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
export function useIsAutheticated() { export function useIsAutheticated() {
const { authState } = useAuth(); const { authState } = useAuth();
return authState.authenticated return authState.authenticated;
} }

View File

@@ -1,9 +0,0 @@
import { Redirect, Link } from "expo-router";
import { Text, View } from "react-native";
import { AuthProvider } from "./context/AuthContext";
function HomePage() {
return <Redirect href="/login" />;
}
export default HomePage;

View File

@@ -1,117 +1,143 @@
import { StyleSheet, TextInput, View, Text, Image } from "react-native"; import { StyleSheet, TextInput, View, Image } from "react-native";
import Text from "@components/Text";
import Link from "@components/Link";
import { LinearGradient } from "expo-linear-gradient";
import { Animated } from "react-native";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { router } from "expo-router"; import { router } from "expo-router";
import Button from "../components/Button"; import Button from "../components/Button";
import { colors } from "../components/style"; import { colors } from "../components/style";
import { useAuth } from "./context/AuthContext"; import { useAuth } from "./context/AuthContext";
function LoginPage() { function LoginPage() {
const [pass, setPass] = useState(""); const [pass, setPass] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const { onLogin, authState } = useAuth(); const { onLogin, authState } = useAuth();
useEffect(() => { useEffect(() => {
if (authState.authenticated) { if (authState.authenticated) {
router.replace("/"); router.replace("/");
} }
}, [authState.authenticated]); }, [authState.authenticated]);
function login() { function login() {
onLogin(email, pass); onLogin(email, pass);
} }
return ( return (
<View style={styles.container}> <View style={styles.container}>
<View style={styles.header}> <AnimatedLinearGradient
<Image colors={[colors.dark, colors.darkSecondary]}
source={require("../assets/deguapp_logo.png")} style={styles.gradient}
style={styles.logo} />
/>
<Text style={styles.h1}>Please Log In</Text> <View style={styles.header}>
</View> <Image
<View style={styles.form}> source={require("../assets/deguapp_logo.png")}
<TextInput style={styles.logo}
style={styles.input} />
placeholder="Enter your email" </View>
autoCapitalize="none" <Text style={styles.h1}>Please Log In</Text>
autoCompleteType="email" <View style={styles.form}>
textContentType="emailAddress" <TextInput
keyboardType="email-address" style={styles.input}
placeholderTextColor={"#aaaaaa"} placeholder="Enter your email"
returnKeyType="next" autoCapitalize="none"
value={email} autoCompleteType="email"
onChangeText={(text) => setEmail(text)} textContentType="emailAddress"
/> keyboardType="email-address"
<TextInput placeholderTextColor={"#aaaaaa"}
style={styles.input} returnKeyType="next"
secureTextEntry={true} value={email}
placeholder="Enter your password" onChangeText={(text) => setEmail(text)}
placeholderTextColor={"#aaaaaa"} />
returnKeyType="done" <TextInput
value={pass} style={styles.input}
onChangeText={(text) => setPass(text)} secureTextEntry={true}
/> placeholder="Enter your password"
<View style={styles.btnContainer}> placeholderTextColor={"#aaaaaa"}
<Button returnKeyType="done"
style={styles.button} value={pass}
title="Sign Up" onChangeText={(text) => setPass(text)}
color={colors.charcoal} />
onPress={() => router.replace("/signup")} <View style={styles.btnContainer}>
/> <Button
<Button style={styles.button}
style={styles.button} title="Log In"
title="Log In" color={colors.gold}
color={colors.gold} onPress={login}
onPress={login} />
/> </View>
</View> <View>
</View> <Link href="/signup" style={styles.signup}>
</View> Don't have an account?
); </Link>
</View>
</View>
</View>
);
} }
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
width: "100%", width: "100%",
height: "100%", height: "100%",
backgroundColor: colors.dark, justifyContent: "center",
}, alignItems: "center",
form: { },
flex: 1, form: {
alignItems: "center", flex: 1,
paddingTop: "10%", alignItems: "center",
width: "100%", width: "100%",
gap: 15, gap: 15,
}, },
h1: { h1: {
color: "#FFF", color: "#FFF",
fontSize: 30, fontSize: 30,
textAlign: "center", textAlign: "center",
paddingTop: "20%", paddingTop: "3%",
}, paddingBottom: "3%",
logo: { },
width: "80%", logo: {
resizeMode: "contain", width: "80%",
}, resizeMode: "contain",
header: { paddingBottom: "0%",
width: "100%", },
alignItems: "center", header: {
paddingTop: "20%", width: "100%",
}, alignItems: "center",
input: { paddingTop: "20%",
height: "auto", },
width: "60%", input: {
borderColor: "gray", height: "auto",
borderWidth: 1, width: "60%",
borderRadius: 5, borderColor: "gray",
padding: 10, borderWidth: 1,
color: "#fff", borderRadius: 10,
}, padding: 13,
btnContainer: { color: "#fff",
flexDirection: "row", },
gap: 5, btnContainer: {
}, flexDirection: "row",
gap: 5,
},
button: {
width: "40%",
},
gradient: {
position: "absolute",
left: 0,
right: 0,
top: 0,
height: "100%",
},
signup: {
textDecorationLine: "underline",
fontSize: 14,
marginTop: 10,
fontStyle: "italic",
},
}); });
export default LoginPage; export default LoginPage;

View File

@@ -3,146 +3,170 @@ import { useState } from "react";
import Button from "../components/Button"; import Button from "../components/Button";
import { colors } from "../components/style"; import { colors } from "../components/style";
import { Link, router } from "expo-router"; import { Link, router } from "expo-router";
import { LinearGradient } from "expo-linear-gradient";
import { Animated } from "react-native";
import { useAuth } from "./context/AuthContext"; import { useAuth } from "./context/AuthContext";
function SignupPage() { function SignupPage() {
const [pass1, setPass1] = useState(""); const [pass1, setPass1] = useState("");
const [pass2, setPass2] = useState(""); const [pass2, setPass2] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const { onSignin } = useAuth(); const { onSignin } = useAuth();
async function signin() { async function signin() {
if (pass1 == pass2) { if (pass1 != pass2) {
const res = await onSignin(username, email, pass1); alert("Passwords are not same!");
if (res.error) { return;
if(res.msg.message == "validation error") { }
alert(res.msg.data.message);
} else {
alert(res.msg.message)
}
}
if (!res.error) {
alert("You have been successfully registered. Please Log In");
router.replace("/login");
}
return;
}
alert("Passwords are not same!"); const res = await onSignin(username, email, pass1);
} const data = await res.json();
return ( if (res.status == 400) {
<View style={styles.container}> if (data.message == "validation error") {
<View style={styles.header}> alert(data.data.message);
<Image } else {
source={require("../assets/deguapp_logo.png")} alert("Something went wrong");
style={styles.logo} }
/> return;
<Text style={styles.h1}>Please Sign Up</Text> }
</View>
<View style={styles.form}> if (res.status == 201) {
<TextInput alert("You have been successfully registered. Please Log In");
style={styles.input} router.replace("/login");
placeholder="Enter your username" return;
placeholderTextColor={"#aaaaaa"} }
returnKeyType="done" }
value={username}
onChangeText={(username) => setUsername(username)} return (
/> <View style={styles.container}>
<TextInput <AnimatedLinearGradient
style={styles.input} colors={[colors.dark, colors.darkSecondary]}
placeholder="Enter your email" style={styles.gradient}
autoCapitalize="none" />
autoCompleteType="email" <View style={styles.header}>
textContentType="emailAddress" <Image
keyboardType="email-address" source={require("../assets/deguapp_logo.png")}
placeholderTextColor={"#aaaaaa"} style={styles.logo}
returnKeyType="next" />
value={email} <Text style={styles.h1}>Please Sign Up</Text>
onChangeText={(email) => setEmail(email)} </View>
/>
<TextInput <View style={styles.form}>
style={styles.input} <TextInput
secureTextEntry={true} style={styles.input}
placeholder="Enter your password" placeholder="Enter your username"
placeholderTextColor={"#aaaaaa"} placeholderTextColor={"#aaaaaa"}
returnKeyType="done" returnKeyType="done"
value={pass1} value={username}
onChangeText={(pass1) => setPass1(pass1)} onChangeText={(username) => setUsername(username)}
/> />
<TextInput <TextInput
style={styles.input} style={styles.input}
secureTextEntry={true} placeholder="Enter your email"
placeholder="Enter your password" autoCapitalize="none"
placeholderTextColor={"#aaaaaa"} autoCompleteType="email"
returnKeyType="done" textContentType="emailAddress"
value={pass2} keyboardType="email-address"
onChangeText={(pass2) => setPass2(pass2)} placeholderTextColor={"#aaaaaa"}
/> returnKeyType="next"
<Button value={email}
style={styles.button} onChangeText={(email) => setEmail(email)}
title="Sign Up" />
color={colors.gold} <TextInput
onPress={signin} style={styles.input}
/> secureTextEntry={true}
<Link href="/login" style={styles.a}> placeholder="Enter your password"
Already have an account? Log In! placeholderTextColor={"#aaaaaa"}
</Link> returnKeyType="done"
</View> value={pass1}
</View> onChangeText={(pass1) => setPass1(pass1)}
); />
<TextInput
style={styles.input}
secureTextEntry={true}
placeholder="Enter your password"
placeholderTextColor={"#aaaaaa"}
returnKeyType="done"
value={pass2}
onChangeText={(pass2) => setPass2(pass2)}
/>
<Button
style={styles.button}
title="Sign Up"
color={colors.gold}
onPress={signin}
/>
<Link href="/login" style={styles.login}>
Already have an account? Log In!
</Link>
</View>
</View>
);
} }
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
width: "100%", width: "100%",
height: "100%", height: "100%",
backgroundColor: colors.dark, backgroundColor: colors.dark,
}, justifyContent: "center",
form: { alignItems: "center",
flex: 1, maxHeight: "100%",
alignItems: "center", },
paddingTop: "10%", form: {
width: "100%", flex: 1,
gap: 15, alignItems: "center",
}, width: "100%",
h1: { gap: 15,
color: "#FFF", },
fontSize: 30, h1: {
textAlign: "center", color: "#FFF",
paddingTop: "20%", fontSize: 30,
}, textAlign: "center",
a: { paddingTop: "3%",
color: "#FFF", paddingBottom: "3%",
fontSize: 12, },
fontStyle: "italic", logo: {
textDecorationLine: "underline", width: "80%",
}, resizeMode: "contain",
logo: { marginTop: "15%",
width: "80%", },
resizeMode: "contain", header: {
}, width: "100%",
header: { alignItems: "center",
width: "100%", },
alignItems: "center", input: {
paddingTop: "20%", height: "auto",
}, width: "60%",
input: { borderColor: "gray",
height: "auto", borderWidth: 1,
width: "60%", borderRadius: 10,
borderColor: "gray", padding: 13,
borderWidth: 1, color: "#fff",
borderRadius: 5, },
padding: 10, btnContainer: {
color: "#fff", flexDirection: "row",
}, gap: 5,
btnContainer: { },
flexDirection: "row", login: {
gap: 5, color: "#FFF",
}, fontStyle: "italic",
textDecorationLine: "underline",
fontSize: 14,
marginTop: 10,
},
gradient: {
position: "absolute",
left: 0,
right: 0,
top: 0,
height: "100%",
},
}); });
export default SignupPage; export default SignupPage;

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -1,6 +1,19 @@
module.exports = function(api) { module.exports = function (api) {
api.cache(true); api.cache(true);
return { return {
presets: ['babel-preset-expo'], presets: ["babel-preset-expo"],
}; plugins: [
[
"module-resolver",
{
root: ["."],
alias: {
"@components": "./components",
"@context": "./app/context",
"@assets": "./assets",
},
},
],
],
};
}; };

15
frontend/biome.json Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
"files": {
"ignore": [".expo/", ".vscode/", "node_modules/", "dist/"]
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": false,
"rules": {
"recommended": true
}
}
}

View File

@@ -2,40 +2,50 @@ import React from "react";
import { Text, StyleSheet, Pressable } from "react-native"; import { Text, StyleSheet, Pressable } from "react-native";
export default function Button(props) { export default function Button(props) {
const { onPress, title = "Save", color = "black" } = props; const {
return ( onPress,
<Pressable title = "Button",
style={({ pressed }) => [ color = "black",
{ textColor = "white",
backgroundColor: pressed buttonStyle,
? "rgb(210, 230, 255 )" textStyle,
: color } = props;
? color return (
: "black", <Pressable
}, style={({ pressed }) => [
styles.button, {
]} backgroundColor: pressed
onPress={onPress} ? "rgb(210, 230, 255 )"
> : color
<Text style={styles.text}>{title}</Text> ? color
</Pressable> : "black",
); },
styles.button,
buttonStyle,
]}
onPress={onPress}
>
<Text style={[styles.text, { color: textColor }, textStyle]}>
{title}
</Text>
</Pressable>
);
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 32, paddingHorizontal: 32,
borderRadius: 4, borderRadius: 4,
elevation: 3, elevation: 3,
}, },
text: { text: {
fontSize: 16, fontSize: 16,
lineHeight: 21, lineHeight: 21,
fontWeight: "bold", fontWeight: "bold",
letterSpacing: 0.25, letterSpacing: 0.25,
color: "white", //color: textColor,
}, },
}); });

View File

@@ -0,0 +1,170 @@
import { StyleSheet } from "react-native";
import { colors } from "@components/style";
export const ICONS = {
ARROW_DOWN: require("@assets/DropdownIcons/arrow-down.png"),
ARROW_UP: require("@assets/DropdownIcons/arrow-up.png"),
TICK: require("@assets/DropdownIcons/tick.png"),
CLOSE: require("../assets/DropdownIcons/close.png"),
};
export default StyleSheet.create({
container: {
width: "100%",
},
style: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
minHeight: 50,
borderRadius: 8,
borderWidth: 1,
borderColor: "gray",
paddingHorizontal: 10,
paddingVertical: 3,
backgroundColor: colors.dark,
},
label: {
flex: 1,
color: colors.placeholder,
},
labelContainer: {
flex: 1,
flexDirection: "row",
},
arrowIcon: {
width: 20,
height: 20,
},
tickIcon: {
width: 20,
height: 20,
},
closeIcon: {
width: 30,
height: 30,
},
badgeStyle: {
flexDirection: "row",
alignItems: "center",
borderRadius: 15,
backgroundColor: colors.white,
paddingHorizontal: 10,
paddingVertical: 5,
},
badgeDotStyle: {
width: 10,
height: 10,
borderRadius: 10 / 2,
marginRight: 8,
backgroundColor: colors.white,
},
badgeSeparator: {
width: 5,
},
listBody: {
height: "100%",
},
listBodyContainer: {
flexGrow: 1,
alignItems: "center",
},
dropDownContainer: {
position: "absolute",
backgroundColor: colors.darkSecondary,
borderRadius: 10,
borderColor: colors.black,
borderWidth: 1,
width: "100%",
overflow: "hidden",
zIndex: 1000,
},
modalContentContainer: {
flexGrow: 1,
backgroundColor: colors.white,
},
listItemContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 10,
height: 40,
},
listItemLabel: {
flex: 1,
color: colors.placeholder,
},
iconContainer: {
marginRight: 10,
},
arrowIconContainer: {
marginLeft: 10,
},
tickIconContainer: {
marginLeft: 10,
},
closeIconContainer: {
marginLeft: 10,
},
listParentLabel: {},
listChildLabel: {},
listParentContainer: {},
listChildContainer: {
paddingLeft: 40,
},
searchContainer: {
flexDirection: "row",
alignItems: "center",
padding: 10,
borderBottomColor: colors.darkSecondary,
borderBottomWidth: 1,
},
searchTextInput: {
flexGrow: 1,
flexShrink: 1,
margin: 0,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 8,
borderColor: colors.darkSecondary,
borderWidth: 1,
color: colors.white,
},
itemSeparator: {
height: 1,
backgroundColor: colors.darkSecondary,
},
flatListContentContainer: {
flexGrow: 1,
},
customItemContainer: {},
customItemLabel: {
fontStyle: "italic",
},
listMessageContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 10,
},
listMessageText: {
color: colors.gold,
},
selectedItemContainer: {},
selectedItemLabel: {},
modalTitle: {
fontSize: 18,
color: colors.gold,
},
extendableBadgeContainer: {
flexDirection: "row",
flexWrap: "wrap",
flex: 1,
},
extendableBadgeItemContainer: {
marginVertical: 3,
marginEnd: 7,
},
});

View File

@@ -0,0 +1,15 @@
import { Link as EXLink } from "expo-router";
const Link = (props) => {
const defaultStyles = {
color: "white",
};
return (
<EXLink style={[defaultStyles, props.style]} href={props.href}>
{props.children}
</EXLink>
);
};
export default Link;

View File

@@ -0,0 +1,13 @@
import React from "react";
import { Text as RNText } from "react-native";
const Text = (props) => {
// Apply your default text color and any other styles here
const defaultStyles = {
color: "white", // Set the default text color to white
};
return <RNText style={[defaultStyles, props.style]}>{props.children}</RNText>;
};
export default Text;

View File

@@ -1,9 +1,12 @@
export const colors = { export const colors = {
gold: "#FFD700ff", gold: "#FFD700ff",
gold: "#ffa500", gold: "#ffa500",
brown: "#8B4513ff", brown: "#8B4513ff",
green: "#228B22ff", green: "#228B22ff",
charcoal: "#2C3E50ff", charcoal: "#2C3E50ff",
black: "#020405ff", black: "#020405ff",
dark: "#010611", dark: "#010409",
darkSecondary: "#0D1117",
white: "#FFFFFF",
placeholder: "#aaaaaa",
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,43 @@
{ {
"name": "deguapp", "name": "deguapp",
"version": "1.0.0", "version": "1.0.0",
"main": "expo-router/entry", "main": "expo-router/entry",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web" "web": "expo start --web",
}, "build:web": "npx expo export",
"dependencies": { "format": "npx @biomejs/biome format --write ."
"@expo/metro-runtime": "~3.1.3", },
"@react-native-async-storage/async-storage": "^1.23.1", "dependencies": {
"@types/react": "~18.2.45", "@expo/metro-runtime": "~3.2.1",
"axios": "^1.6.8", "@react-native-async-storage/async-storage": "^1.23.1",
"expo": "~50.0.17", "@types/react": "~18.2.45",
"expo-constants": "~15.4.6", "axios": "^1.6.8",
"expo-linking": "~6.2.2", "expo": "^51.0.2",
"expo-router": "~3.4.10", "expo-constants": "~16.0.1",
"expo-secure-store": "^12.8.1", "expo-image-picker": "~15.0.4",
"expo-status-bar": "~1.11.1", "expo-linear-gradient": "~13.0.2",
"react": "18.2.0", "expo-linking": "~6.3.1",
"react-dom": "18.2.0", "expo-router": "~3.5.11",
"react-native": "0.73.6", "expo-secure-store": "~13.0.1",
"react-native-safe-area-context": "4.8.2", "expo-status-bar": "~1.12.1",
"react-native-screens": "~3.29.0", "expo-system-ui": "~3.0.4",
"react-native-web": "~0.19.6" "react": "18.2.0",
}, "react-dom": "18.2.0",
"devDependencies": { "react-native": "0.74.1",
"@babel/core": "^7.20.0" "react-native-dropdown-picker": "^5.4.6",
}, "react-native-range-slider-expo": "^1.4.3",
"private": true "react-native-safe-area-context": "4.10.1",
"react-native-screens": "3.31.1",
"react-native-web": "~0.19.6",
"@shopify/flash-list": "1.6.4"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@biomejs/biome": "1.7.3",
"babel-plugin-module-resolver": "^5.0.2"
},
"private": true
} }