diff --git a/api/package.json b/api/package.json index 7eb6030f6..f1c8996f5 100644 --- a/api/package.json +++ b/api/package.json @@ -28,6 +28,7 @@ "@prisma/client": "5.16.2", "@sentry/node": "6.19.6", "@sentry/tracing": "6.17.6", + "bcrypt": "5.1.1", "cors": "2.8.5", "cross-env": "7.0.3", "data-forge": "1.10.2", @@ -38,12 +39,14 @@ "express": "4.18.2", "geoip-lite": "1.4.10", "helmet": "4.0.0", + "jsonwebtoken": "9.0.2", "morgan": "1.10.0", "node-cron": "3.0.2", "node-fetch": "2.6.7", "node-pushnotifications": "^3.1.1", "prisma": "5.16.2", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "validator": "13.12.0" }, "devDependencies": { "@types/node": "^20.14.10", diff --git a/api/prisma/migrations/20240821100937_add_email_password_to_user/migration.sql b/api/prisma/migrations/20240821100937_add_email_password_to_user/migration.sql new file mode 100644 index 000000000..baa456b89 --- /dev/null +++ b/api/prisma/migrations/20240821100937_add_email_password_to_user/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail. + - Added the required column `email` to the `User` table without a default value. This is not possible if the table is not empty. + - Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "email" TEXT NOT NULL, +ADD COLUMN "password" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 17725b3cd..1f9aaea70 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -14,6 +14,8 @@ datasource db { model User { id String @id @default(uuid()) matomo_id String @unique + email String @unique + password String push_notif_token String @default("") reminder Reminder? notifications Notification[] diff --git a/api/src/config.js b/api/src/config.js index c4e0d4c3c..7627576e0 100644 --- a/api/src/config.js +++ b/api/src/config.js @@ -25,6 +25,7 @@ const MATOMO_URL = process.env.MATOMO_URL; const MATOMO_IDSITE_1 = process.env.MATOMO_IDSITE_1; const METABASE_ACCOUNT = process.env.METABASE_ACCOUNT; const METABASE_PASSWORD = process.env.METABASE_PASSWORD; +const JWT_SECRET = process.env.JWT_SECRET; module.exports = { PORT, @@ -46,4 +47,5 @@ module.exports = { MATOMO_IDSITE_1, METABASE_ACCOUNT, METABASE_PASSWORD, + JWT_SECRET, }; diff --git a/api/src/controllers/appMilestone.js b/api/src/controllers/appMilestone.js index 76c596d67..7d9c49f71 100644 --- a/api/src/controllers/appMilestone.js +++ b/api/src/controllers/appMilestone.js @@ -17,6 +17,8 @@ router.post( create: { matomo_id: matomoId, created_from: "AppMilestonePost", + email: "yoan.roszak@selego.co", + password: "password12@Abc", }, update: {}, }); @@ -48,6 +50,8 @@ router.post( create: { matomo_id: matomoId, created_from: "AppMilestoneInit", + email: "yoan.roszak@selego.co", + password: "password12@Abc", }, update: {}, }); diff --git a/api/src/controllers/articles.js b/api/src/controllers/articles.js index 21ab08ec9..3228ee78c 100644 --- a/api/src/controllers/articles.js +++ b/api/src/controllers/articles.js @@ -16,6 +16,8 @@ router.post( create: { matomo_id: matomoId, created_from: "Articles", + email: "yoan.roszak@selego.co", + password: "password12@Abc", }, update: {}, }); diff --git a/api/src/controllers/badge.js b/api/src/controllers/badge.js index 5472ceaa4..96d9242aa 100644 --- a/api/src/controllers/badge.js +++ b/api/src/controllers/badge.js @@ -64,8 +64,11 @@ router.post( where: { matomo_id: matomoId }, create: { matomo_id: matomoId, + email: "yoan.roszak@selego.co", + password: "password12@Abc", created_from: "GetBadges", }, + update: {}, }); const share_badges = await prisma.badge.findMany({ diff --git a/api/src/controllers/consommation.js b/api/src/controllers/consommation.js index e7ce65c31..ad6413b41 100644 --- a/api/src/controllers/consommation.js +++ b/api/src/controllers/consommation.js @@ -37,6 +37,8 @@ router.post( where: { matomo_id: matomoId }, create: { matomo_id: matomoId, + email: "yoan.roszak@selego.co", + password: "password12@Abc", }, update: {}, }); diff --git a/api/src/controllers/defis.js b/api/src/controllers/defis.js index d5a24727c..c26f8dde6 100644 --- a/api/src/controllers/defis.js +++ b/api/src/controllers/defis.js @@ -23,6 +23,8 @@ router.post( where: { matomo_id: matomoId }, create: { matomo_id: matomoId, + email: "yoan.roszak@selego.co", + password: "password12@Abc", created_from: "Defis", }, update: {}, diff --git a/api/src/controllers/drinksContext.js b/api/src/controllers/drinksContext.js index 155c78999..be0f49f43 100644 --- a/api/src/controllers/drinksContext.js +++ b/api/src/controllers/drinksContext.js @@ -72,6 +72,8 @@ router.post( const user = await prisma.user.upsert({ where: { matomo_id: matomoId }, create: { + email: "yoan.roszak@selego.co", + password: "password12@Abc", matomo_id: matomoId, }, update: {}, diff --git a/api/src/controllers/event.js b/api/src/controllers/event.js index c766f4c77..9de32143f 100644 --- a/api/src/controllers/event.js +++ b/api/src/controllers/event.js @@ -36,6 +36,8 @@ router.post( where: { matomo_id: matomoId }, create: { matomo_id: matomoId, + email: "yoan.roszak@selego.co", + password: "password12@Abc", created_from: "EventUserSurveyStarted", }, update: {}, @@ -71,6 +73,8 @@ router.post( where: { matomo_id: matomoId }, create: { matomo_id: matomoId, + email: "yoan.roszak@selego.co", + password: "password12@Abc", created_from: "EventUserSurveyFinished", }, update: {}, diff --git a/api/src/controllers/goal.js b/api/src/controllers/goal.js index 7c714e106..8762b7663 100644 --- a/api/src/controllers/goal.js +++ b/api/src/controllers/goal.js @@ -21,6 +21,8 @@ router.post( where: { matomo_id: matomoId }, create: { matomo_id: matomoId, + email: "yoan.roszak@selego.co", + password: "password12@Abc", created_from: "Goal", goal_isSetup: true, goal_daysWithGoalNoDrink: daysWithGoalNoDrink, @@ -111,6 +113,8 @@ router.get( where: { matomo_id: matomoId }, create: { matomo_id: matomoId, + email: "yoan.roszak@selego.co", + password: "password12@Abc", created_from: "GetGoal", }, update: {}, diff --git a/api/src/controllers/reminder.js b/api/src/controllers/reminder.js index fab122df2..8bdad9661 100644 --- a/api/src/controllers/reminder.js +++ b/api/src/controllers/reminder.js @@ -98,6 +98,8 @@ router.put( push_notif_token: pushNotifToken, }, create: { + email: "yoan.roszak@selego.co", + password: "password12@Abc", push_notif_token: pushNotifToken, matomo_id: matomoId, created_from: "Reminder", diff --git a/api/src/controllers/test.js b/api/src/controllers/test.js index 338200f59..1054e4d93 100644 --- a/api/src/controllers/test.js +++ b/api/src/controllers/test.js @@ -63,6 +63,8 @@ router.post( let user = await prisma.user.upsert({ where: { matomo_id: matomoId }, create: { + email: "yoan.roszak@selego.co", + password: "password12@Abc", matomo_id: matomoId, created_from: "test", }, diff --git a/api/src/controllers/user.js b/api/src/controllers/user.js index f86734a33..0962f716a 100644 --- a/api/src/controllers/user.js +++ b/api/src/controllers/user.js @@ -3,6 +3,128 @@ const { catchErrors } = require("../middlewares/errors"); const router = express.Router(); const prisma = require("../prisma"); const geoip = require("geoip-lite"); +const bcrypt = require("bcrypt"); +const validator = require("validator"); +const { isStrongPassword } = require("validator"); +const jwt = require("jsonwebtoken"); +const { JWT_SECRET } = require("../config"); + +// 1 year +const JWT_MAX_AGE = "365d"; + +function validatePassword(password) { + return isStrongPassword(password, { + minLength: 12, + minLowercase: 1, + minUppercase: 1, + minNumbers: 1, + minSymbols: 1, + }); +} + +router.post( + "/signup", + catchErrors(async (req, res) => { + const { email, password, matomoId } = req.body || {}; + if (!matomoId) return res.status(400).json({ ok: false, error: "no matomo id" }); + + if (!email || !password) { + return res.status(400).json({ ok: false, error: "missing email or password" }); + } + + if (!validator.isEmail(email)) { + return res.status(400).json({ ok: false, error: "invalid email" }); + } + if (!validatePassword(password)) { + return res.status(400).json({ ok: false, error: "password is not strong enough" }); + } + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (user) { + return res.status(400).json({ ok: false, error: "email already exists" }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + await prisma.user.upsert({ + where: { matomo_id: matomoId }, + update: updateObj, + create: { + email, + password: hashedPassword, + matomo_id: matomoId, + created_from, + ...updateObj, + }, + }); + + const token = jwt.sign({ email }, JWT_SECRET, { + expiresIn: JWT_MAX_AGE, + }); + + return res.status(200).send({ ok: true, token }); + }) +); + +router.post( + "/signin", + catchErrors(async (req, res) => { + const { email, password, matomoId } = req.body || {}; + validator.isEmail(email); + validator.isStrongPassword(password); + console.log("signin", email, password, matomoId); + if (!matomoId) return res.status(400).json({ ok: false, error: "no matomo id" }); + + if (!email || !password) { + return res.status(400).json({ ok: false, error: "missing email or password" }); + } + + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user) { + return res.status(400).json({ ok: false, error: "wrong email or password" }); + } + console.log("user", user); + + // const match = await bcrypt.compare(password, user.password); + const match = password === user.password; + + if (!match) { + return res.status(400).json({ ok: false, error: "wrong email or password" }); + } + + const token = jwt.sign({ email }, JWT_SECRET, { + expiresIn: JWT_MAX_AGE, + }); + + return res.status(200).send({ ok: true, token }); + }) +); + +router.get( + "/signin_token", + catchErrors(async (req, res) => { + const token = req.headers.authorization?.split(" ")[1]; // Bearer token extraction + if (!token) { + return res.status(401).json({ ok: false, error: "No token provided" }); + } + const decoded = jwt.verify(token, JWT_SECRET); + + const user = await prisma.user.findUnique({ + where: { email: decoded.email }, + }); + + if (!user) { + return res.status(400).json({ ok: false, error: "user not found" }); + } + + return res.status(200).send({ ok: true, user, token }); + }) +); router.put( "/", @@ -30,6 +152,8 @@ router.put( update: updateObj, create: { matomo_id: matomoId, + email: "yoan.roszak@selego.co", + password: "password12@Abc", created_from, ...updateObj, }, diff --git a/api/src/utils/notifications.js b/api/src/utils/notifications.js index b2910fcbe..3a7000162 100644 --- a/api/src/utils/notifications.js +++ b/api/src/utils/notifications.js @@ -14,6 +14,8 @@ const updateLastConsoAdded = async (matomoId) => { where: { matomo_id: matomoId }, create: { matomo_id: matomoId, + email: "yoan.roszak@selego.co", + password: "password12@Abc", lastConsoAdded: dayjs().utc().toDate(), created_from: "UpdateLastConso", }, @@ -318,7 +320,7 @@ const scheduleDefi1Day1 = async (matomoId) => { const type = "DEFI1_DAY1"; const user = await prisma.user.upsert({ where: { matomo_id: matomoId }, - create: { matomo_id: matomoId, created_from: "SheduleDefiDay1" }, + create: { matomo_id: matomoId, created_from: "SheduleDefiDay1", email: "yoan.roszak@selego.co", password: "password12@Abc" }, update: {}, }); @@ -356,7 +358,7 @@ const scheduleUserSurvey = async (matomoId) => { const type = "USER_SURVEY"; const user = await prisma.user.upsert({ where: { matomo_id: matomoId }, - create: { matomo_id: matomoId, created_from: "UserSurvey" }, + create: { matomo_id: matomoId, created_from: "UserSurvey", email: "yoan.roszak@selego.co", password: "password12@Abc" }, update: {}, }); diff --git a/api/yarn.lock b/api/yarn.lock index f072319af..b0e0ae4aa 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -919,6 +919,25 @@ __metadata: languageName: node linkType: hard +"@mapbox/node-pre-gyp@npm:^1.0.11": + version: 1.0.11 + resolution: "@mapbox/node-pre-gyp@npm:1.0.11" + dependencies: + detect-libc: "npm:^2.0.0" + https-proxy-agent: "npm:^5.0.0" + make-dir: "npm:^3.1.0" + node-fetch: "npm:^2.6.7" + nopt: "npm:^5.0.0" + npmlog: "npm:^5.0.1" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.11" + bin: + node-pre-gyp: bin/node-pre-gyp + checksum: 2b24b93c31beca1c91336fa3b3769fda98e202fb7f9771f0f4062588d36dcc30fcf8118c36aa747fa7f7610d8cf601872bdaaf62ce7822bb08b545d1bbe086cc + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1542,6 +1561,13 @@ __metadata: languageName: node linkType: hard +"abbrev@npm:1": + version: 1.1.1 + resolution: "abbrev@npm:1.1.1" + checksum: 3f762677702acb24f65e813070e306c61fafe25d4b2583f9dfc935131f774863f3addd5741572ed576bd69cabe473c5af18e1e108b829cb7b6b4747884f726e6 + languageName: node + linkType: hard + "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -1695,6 +1721,7 @@ __metadata: "@types/node": "npm:^20.14.10" babel-cli: "npm:6.26.0" babel-preset-env: "npm:1.7.0" + bcrypt: "npm:5.1.1" cors: "npm:2.8.5" cross-env: "npm:7.0.3" data-forge: "npm:1.10.2" @@ -1706,6 +1733,7 @@ __metadata: geoip-lite: "npm:1.4.10" helmet: "npm:4.0.0" jest: "npm:29.7.0" + jsonwebtoken: "npm:9.0.2" knip: "npm:^5.25.2" morgan: "npm:1.10.0" node-cron: "npm:3.0.2" @@ -1717,9 +1745,27 @@ __metadata: supertest: "npm:6.3.4" typescript: "npm:^5.5.3" uuid: "npm:^10.0.0" + validator: "npm:13.12.0" languageName: unknown linkType: soft +"aproba@npm:^1.0.3 || ^2.0.0": + version: 2.0.0 + resolution: "aproba@npm:2.0.0" + checksum: d06e26384a8f6245d8c8896e138c0388824e259a329e0c9f196b4fa533c82502a6fd449586e3604950a0c42921832a458bb3aa0aa9f0ba449cfd4f50fd0d09b5 + languageName: node + linkType: hard + +"are-we-there-yet@npm:^2.0.0": + version: 2.0.0 + resolution: "are-we-there-yet@npm:2.0.0" + dependencies: + delegates: "npm:^1.0.0" + readable-stream: "npm:^3.6.0" + checksum: 375f753c10329153c8d66dc95e8f8b6c7cc2aa66e05cb0960bd69092b10dae22900cacc7d653ad11d26b3ecbdbfe1e8bfb6ccf0265ba8077a7d979970f16b99c + languageName: node + linkType: hard + "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -2654,6 +2700,16 @@ __metadata: languageName: node linkType: hard +"bcrypt@npm:5.1.1": + version: 5.1.1 + resolution: "bcrypt@npm:5.1.1" + dependencies: + "@mapbox/node-pre-gyp": "npm:^1.0.11" + node-addon-api: "npm:^5.0.0" + checksum: 743231158c866bddc46f25eb8e9617fe38bc1a6f5f3052aba35e361d349b7f8fb80e96b45c48a4c23c45c29967ccd11c81cf31166454fc0ab019801c336cab40 + languageName: node + linkType: hard + "bignumber.js@npm:^9.0.0": version: 9.1.2 resolution: "bignumber.js@npm:9.1.2" @@ -3131,6 +3187,15 @@ __metadata: languageName: node linkType: hard +"color-support@npm:^1.1.2": + version: 1.1.3 + resolution: "color-support@npm:1.1.3" + bin: + color-support: bin.js + checksum: 8ffeaa270a784dc382f62d9be0a98581db43e11eee301af14734a6d089bd456478b1a8b3e7db7ca7dc5b18a75f828f775c44074020b51c05fc00e6d0992b1cc6 + languageName: node + linkType: hard + "combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" @@ -3168,6 +3233,13 @@ __metadata: languageName: node linkType: hard +"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": + version: 1.1.0 + resolution: "console-control-strings@npm:1.1.0" + checksum: 7ab51d30b52d461412cd467721bb82afe695da78fff8f29fe6f6b9cbaac9a2328e27a22a966014df9532100f6dd85370460be8130b9c677891ba36d96a343f50 + languageName: node + linkType: hard + "content-disposition@npm:0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" @@ -3490,6 +3562,13 @@ __metadata: languageName: node linkType: hard +"delegates@npm:^1.0.0": + version: 1.0.0 + resolution: "delegates@npm:1.0.0" + checksum: ba05874b91148e1db4bf254750c042bf2215febd23a6d3cda2e64896aef79745fbd4b9996488bd3cafb39ce19dbce0fd6e3b6665275638befffe1c9b312b91b5 + languageName: node + linkType: hard + "depd@npm:2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -4319,6 +4398,23 @@ __metadata: languageName: node linkType: hard +"gauge@npm:^3.0.0": + version: 3.0.2 + resolution: "gauge@npm:3.0.2" + dependencies: + aproba: "npm:^1.0.3 || ^2.0.0" + color-support: "npm:^1.1.2" + console-control-strings: "npm:^1.0.0" + has-unicode: "npm:^2.0.1" + object-assign: "npm:^4.1.1" + signal-exit: "npm:^3.0.0" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + wide-align: "npm:^1.1.2" + checksum: 75230ccaf216471e31025c7d5fcea1629596ca20792de50c596eb18ffb14d8404f927cd55535aab2eeecd18d1e11bd6f23ec3c2e9878d2dda1dc74bccc34b913 + languageName: node + linkType: hard + "gaxios@npm:^6.0.0, gaxios@npm:^6.0.2, gaxios@npm:^6.1.1": version: 6.7.0 resolution: "gaxios@npm:6.7.0" @@ -4590,6 +4686,13 @@ __metadata: languageName: node linkType: hard +"has-unicode@npm:^2.0.1": + version: 2.0.1 + resolution: "has-unicode@npm:2.0.1" + checksum: ebdb2f4895c26bb08a8a100b62d362e49b2190bcfd84b76bc4be1a3bd4d254ec52d0dd9f2fbcc093fc5eb878b20c52146f9dfd33e2686ed28982187be593b47c + languageName: node + linkType: hard + "has-value@npm:^0.3.1": version: 0.3.1 resolution: "has-value@npm:0.3.1" @@ -5811,7 +5914,7 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:^9.0.0": +"jsonwebtoken@npm:9.0.2, jsonwebtoken@npm:^9.0.0": version: 9.0.2 resolution: "jsonwebtoken@npm:9.0.2" dependencies: @@ -6115,6 +6218,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^3.1.0": + version: 3.1.0 + resolution: "make-dir@npm:3.1.0" + dependencies: + semver: "npm:^6.0.0" + checksum: 56aaafefc49c2dfef02c5c95f9b196c4eb6988040cf2c712185c7fe5c99b4091591a7fc4d4eafaaefa70ff763a26f6ab8c3ff60b9e75ea19876f49b18667ecaa + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -6565,7 +6677,7 @@ __metadata: languageName: node linkType: hard -"node-addon-api@npm:^5.1.0": +"node-addon-api@npm:^5.0.0, node-addon-api@npm:^5.1.0": version: 5.1.0 resolution: "node-addon-api@npm:5.1.0" dependencies: @@ -6604,7 +6716,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.9": +"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -6705,6 +6817,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:^5.0.0": + version: 5.0.0 + resolution: "nopt@npm:5.0.0" + dependencies: + abbrev: "npm:1" + bin: + nopt: bin/nopt.js + checksum: fc5c4f07155cb455bf5fc3dd149fac421c1a40fd83c6bfe83aa82b52f02c17c5e88301321318adaa27611c8a6811423d51d29deaceab5fa158b585a61a551061 + languageName: node + linkType: hard + "nopt@npm:^7.0.0": version: 7.2.1 resolution: "nopt@npm:7.2.1" @@ -6741,6 +6864,18 @@ __metadata: languageName: node linkType: hard +"npmlog@npm:^5.0.1": + version: 5.0.1 + resolution: "npmlog@npm:5.0.1" + dependencies: + are-we-there-yet: "npm:^2.0.0" + console-control-strings: "npm:^1.1.0" + gauge: "npm:^3.0.0" + set-blocking: "npm:^2.0.0" + checksum: 489ba519031013001135c463406f55491a17fc7da295c18a04937fe3a4d523fd65e88dd418a28b967ab743d913fdeba1e29838ce0ad8c75557057c481f7d49fa + languageName: node + linkType: hard + "numeral@npm:^2.0.6": version: 2.0.6 resolution: "numeral@npm:2.0.6" @@ -6748,7 +6883,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4, object-assign@npm:^4.1.0": +"object-assign@npm:^4, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 @@ -7343,7 +7478,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0": +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -7604,6 +7739,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -7652,7 +7798,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.3.0, semver@npm:^6.3.1": +"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -7703,6 +7849,13 @@ __metadata: languageName: node linkType: hard +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 9f8c1b2d800800d0b589de1477c753492de5c1548d4ade52f57f1d1f5e04af5481554d75ce5e5c43d4004b80a3eb714398d6907027dc0534177b7539119f4454 + languageName: node + linkType: hard + "set-function-length@npm:^1.2.1": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -7764,7 +7917,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 @@ -8041,7 +8194,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -8645,6 +8798,13 @@ __metadata: languageName: node linkType: hard +"validator@npm:13.12.0": + version: 13.12.0 + resolution: "validator@npm:13.12.0" + checksum: 21d48a7947c9e8498790550f56cd7971e0e3d724c73388226b109c1bac2728f4f88caddfc2f7ed4b076f9b0d004316263ac786a17e9c4edf075741200718cd32 + languageName: node + linkType: hard + "vary@npm:^1, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -8753,6 +8913,15 @@ __metadata: languageName: node linkType: hard +"wide-align@npm:^1.1.2": + version: 1.1.5 + resolution: "wide-align@npm:1.1.5" + dependencies: + string-width: "npm:^1.0.2 || 2 || 3 || 4" + checksum: 1d9c2a3e36dfb09832f38e2e699c367ef190f96b82c71f809bc0822c306f5379df87bab47bed27ea99106d86447e50eb972d3c516c2f95782807a9d082fbea95 + languageName: node + linkType: hard + "wns@npm:0.5.4": version: 0.5.4 resolution: "wns@npm:0.5.4" diff --git a/expo/src/Router.js b/expo/src/Router.js index b0a9e8253..ab1549d83 100644 --- a/expo/src/Router.js +++ b/expo/src/Router.js @@ -49,6 +49,8 @@ import { isInCravingKeyState } from "./recoil/craving"; import { dayjsInstance } from "./services/dates"; import SuccessStrategyModal from "./scenes/Craving/SuccessStrategyModal"; import ExportedDataDone from "./scenes/Craving/ExportedDataDone"; +import SigninScreen from "./scenes/Auth/Signin"; +import SignupScreen from "./scenes/Auth/Signup"; const Label = ({ children, focused, color }) => ( @@ -166,7 +168,9 @@ const TabsNavigator = ({ navigation }) => { const AppStack = createNativeStackNavigator(); const App = () => { const initialRouteName = useMemo(() => { + const token = storage.getString("@Token"); const onBoardingDone = storage.getBoolean("@OnboardingDoneWithCGU"); + if (token) return "SIGNIN_SCREEN"; if (!onBoardingDone) return "WELCOME"; return "TABS"; }, []); @@ -174,6 +178,8 @@ const App = () => { return ( <> + + diff --git a/expo/src/scenes/Auth/Signin.js b/expo/src/scenes/Auth/Signin.js new file mode 100644 index 000000000..81644ef41 --- /dev/null +++ b/expo/src/scenes/Auth/Signin.js @@ -0,0 +1,96 @@ +import React, { useState } from "react"; +import ButtonPrimary from "../../components/ButtonPrimary"; +import { Image, View, TextInput, TouchableOpacity, Text } from "react-native"; +import Wave from "../../components/illustrations/onboarding/Wave"; +import { screenWidth } from "../../styles/theme"; +import API from "../../services/api"; +import { storage } from "../../services/storage"; +import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; + +const SigninScreen = ({ navigation }) => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [hidePassword, setHidePassword] = useState(true); + const signin = async () => { + const response = await API.post({ + path: "user/signin", + body: { + matomoId: storage.getString("@UserIdv2"), + email: email.toLowerCase(), + password, + }, + }); + if (response.ok) { + storage.set("@Token", response.token); + const onBoardingDone = storage.getBoolean("@OnboardingDoneWithCGU"); + if (!onBoardingDone) navigation.push("WELCOME_SWIPER"); + + navigation.push("TABS"); + } else { + alert("Erreur lors de la connexion"); + } + }; + + return ( + + + + + + + + E-mail + + Mot de passe + + + setHidePassword(!hidePassword)}> + {hidePassword ? ( + + ) : ( + + )} + + + + + navigation.push("FORGOT_PASSWORD")}> + Mot de passe oublié ? + + + + + + + + { + signin(); + }} + /> + navigation.push("WELCOME")}> + Créer un compte + + + + ); +}; + +export default SigninScreen; diff --git a/expo/src/scenes/Auth/Signup.js b/expo/src/scenes/Auth/Signup.js new file mode 100644 index 000000000..f78092fa7 --- /dev/null +++ b/expo/src/scenes/Auth/Signup.js @@ -0,0 +1,82 @@ +import React, { useState } from "react"; +import ButtonPrimary from "../../components/ButtonPrimary"; +import { Image, View, TextInput, TouchableOpacity, Text } from "react-native"; +import Wave from "../../components/illustrations/onboarding/Wave"; +import { screenWidth } from "../../styles/theme"; +import API from "../../services/api"; +import { storage } from "../../services/storage"; +import { initMatomo } from "../../services/logEventsWithMatomo"; + +const SignupScreen = ({ navigation }) => { + const matomoId = storage.getString("@UserIdv2"); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const signup = async () => { + initMatomo(email, password); + const matomoId = storage.getString("@UserIdv2"); + const response = await API.post("user/signup", { + matomoId, + email, + password, + }); + if (response.ok) { + storage.set("@Email", email); + storage.set("@Token", response.token); + navigation.push("WELCOME_SWIPER"); + } else { + alert("Erreur lors de l'inscription"); + } + }; + + return ( + + + + + + + + Email + + Mot de passe + + + + navigation.push("FORGOT_PASSWORD")}> + Mot de passe oublié ? + + + + + + + + { + signup(); + }} + /> + navigation.push("SIGNUP")}> + Créer un compte + + + + ); +}; + +export default SignupScreen; diff --git a/expo/src/scenes/Infos/Transfer.js b/expo/src/scenes/Infos/Transfer.js index 128e38ea6..0d919b87a 100644 --- a/expo/src/scenes/Infos/Transfer.js +++ b/expo/src/scenes/Infos/Transfer.js @@ -4,7 +4,6 @@ import WrapperContainer from "../../components/WrapperContainer.js"; import { storage } from "../../services/storage.js"; import API from "../../services/api.js"; import * as DocumentPicker from "expo-document-picker"; -import * as Expo from "expo"; import RNFS from "react-native-fs"; import Share from "react-native-share"; import TipIcon from "../../components/illustrations/TipIcon.js"; diff --git a/expo/src/scenes/WelcomeScreen/ScreensOldUser.js b/expo/src/scenes/WelcomeScreen/ScreensOldUser.js new file mode 100644 index 000000000..6bdc6ca1b --- /dev/null +++ b/expo/src/scenes/WelcomeScreen/ScreensOldUser.js @@ -0,0 +1,178 @@ +import React from "react"; +import Agreement from "./Agreement"; +import { Image, View, Text, TouchableOpacity } from "react-native"; +import { screenWidth } from "../../styles/theme"; +import Wave from "../../components/illustrations/onboarding/Wave"; +import ButtonPrimary from "../../components/ButtonPrimary"; +import DownloadIcon from "../../components/illustrations/icons/DownloadIcon.js"; +import { logEvent } from "../../services/logEventsWithMatomo"; +import { storage } from "../../services/storage"; +import * as DocumentPicker from "expo-document-picker"; +import { Alert } from "react-native"; +import RNRestart from "react-native-restart"; +import API from "../../services/api"; + +export const StepOne = ({ onPressNext }) => ( + + + + Importer les données de Oz {"\n"} Ensemble + + + + Etape 1 + + + Sauvegardez les données de votre profil depuis votre application Oz Ensemble, de la manière suivante : + + + 1. + + Ouvrez votre application Oz Ensemble, + + + + 2. + + Allez sur la page “Infos” (en bas à droite dans la barre de menu), + + + + 3. + + Dans la section “Mes paramètres”, allez sur + “Sauvegarder les données de mon profil” , + + + + 4. + + Suivez les étapes indiquées et sauvegardez les informations de votre profil sur votre téléphone. + + + + + + + + + + + +); + +export const StepTwo = ({ onPressNext }) => ( + + + + Importer les données de Oz {"\n"} Ensemble + + + + Etape 2 + + + Importez les données de votre profil (que vous avez précédemment sauvegardées à l’étape 1), en suivant les + étapes suivantes : + + + 1. + Cliquez sur le bouton “Importer mes données Oz” ci-dessous, + + + 2. + + Une fenêtre s’ouvre, sélectionnez le fichier nommé “export-oz-ensemble” que vous avez sauvegardé sur votre + téléphone et importez-le. + + + + + + + + + + + + Importer mes données Oz + + + + +); + +export const Validation = ({ onStartPress, agreed, setAgreed }) => ( + + + + Recevez des conseils personnalisés + + + + + + + + + + setAgreed(!agreed)} agreed={agreed} className="" /> + + + +); +const importData = async () => { + logEvent({ category: "TRANSFER", action: "IMPORT_DATA" }); + try { + const result = await DocumentPicker.getDocumentAsync({ type: "application/json" }); + const fileUri = result.assets && result.assets.length > 0 ? result.assets[0].uri : undefined; + const fileContents = await fetch(fileUri).then((response) => response.text()); + const pushNotifToken = storage.getString("STORAGE_KEY_PUSH_NOTIFICATION_TOKEN"); + await overwriteData(fileContents, pushNotifToken); + } catch (err) { + if (DocumentPicker.isCancel(err)) { + logEvent({ category: "TRANSFER", action: "CANCEL_IMPORT_DATA", name: "DOCUMENT_PICKER_CANCEL" }); + } else { + console.log(err); + } + } +}; + +const overwriteData = async (dataImported, pushNotifToken) => { + try { + if (Object.keys(dataImported).length === 0) { + throw new Error("Imported data is empty"); + } + storage.clearAll(); + if (pushNotifToken) { + storage.set("STORAGE_KEY_PUSH_NOTIFICATION_TOKEN", pushNotifToken); + } + Object.keys(dataImported).forEach((key) => { + const value = dataImported[key]; + if (typeof value === "object") { + storage.set(key, JSON.stringify(value)); + } else { + storage.set(key, value); + } + }); + + await API.put({ path: `/user`, body: { matomoId: dataImported["@UserIdv2"], pushNotifToken } }).then((res) => { + if (res.ok) { + logEvent({ category: "TRANSFER", action: "IMPORT_DATA_SUCCESS" }); + } else { + logEvent({ category: "TRANSFER", action: "IMPORT_DATA_SUCCESS", name: "PUSH_NOTIF_TOKEN_NOT_SYNC" }); + } + onPressNext(); + }); + } catch (error) { + Alert.alert("Une erreur est survenue lors de l'importation des données"); + logEvent({ category: "TRANSFER", action: "IMPORT_DATA_FAILURE" }); + } +}; diff --git a/expo/src/scenes/WelcomeScreen/WelcomeStart.js b/expo/src/scenes/WelcomeScreen/WelcomeStart.js index f4afd6efa..9bcdce14d 100644 --- a/expo/src/scenes/WelcomeScreen/WelcomeStart.js +++ b/expo/src/scenes/WelcomeScreen/WelcomeStart.js @@ -11,22 +11,14 @@ const WelcomeStart = ({ navigation }) => { - - Bienvenue sur Oz ! - + Bienvenue sur Oz ! - Vous avez entre les mains un outil{" "} - gratuit et{" "} - anonyme de suivi de - consommation d’alcool + Vous avez entre les mains un outil gratuit et{" "} + anonyme de suivi de consommation d’alcool - + @@ -43,7 +35,7 @@ const WelcomeStart = ({ navigation }) => { content="Commencer" AnimationEffect onPress={() => { - navigation.push("WELCOME_SWIPER"); + navigation.push("WELCOME_USER_TYPE"); }} /> diff --git a/expo/src/scenes/WelcomeScreen/WelcomeSwiperOldUser.js b/expo/src/scenes/WelcomeScreen/WelcomeSwiperOldUser.js new file mode 100644 index 000000000..d6f6b6568 --- /dev/null +++ b/expo/src/scenes/WelcomeScreen/WelcomeSwiperOldUser.js @@ -0,0 +1,52 @@ +import React, { useRef, useState } from "react"; +import Swiper from "react-native-swiper"; +import { storage } from "../../services/storage"; +import { StepOne, StepTwo, Validation } from "./ScreensOldUser"; +import Dot from "../../components/SwiperDot"; +import { View } from "react-native"; + +const WelcomeSwiperOldUser = ({ navigation }) => { + const [agreed, setAgreed] = useState(false); + const [, setCurrentIndex] = useState(0); + // const [pagination, setPagination] = useState(true); + const swiperRef = useRef(); + + const indexChanged = (index) => { + setCurrentIndex(index); + }; + + const onStartPress = async () => { + storage.set("@OnboardingDoneWithCGU", true); + navigation.navigate("USER_SURVEY_START", { from: "NEW_USER" }); + }; + + const onPressNext = () => swiperRef?.current?.scrollBy(1); + + return ( + + + + } + activeDot={} + paginationStyle={{ + justifyContent: "center", + bottom: 108, + }} + > + + + + + + + ); +}; + +export default WelcomeSwiperOldUser; diff --git a/expo/src/scenes/WelcomeScreen/WelcomeUserType.js b/expo/src/scenes/WelcomeScreen/WelcomeUserType.js new file mode 100644 index 000000000..a3601309f --- /dev/null +++ b/expo/src/scenes/WelcomeScreen/WelcomeUserType.js @@ -0,0 +1,50 @@ +import React from "react"; +import ButtonPrimary from "../../components/ButtonPrimary"; +import { Image, View, Text } from "react-native"; +import Wave from "../../components/illustrations/onboarding/Wave"; +import { screenWidth } from "../../styles/theme"; + +const WelcomeUserType = ({ navigation }) => { + return ( + + + + + + + Etes-vous un utilisateur de l’app OZ Ensemble ? + + + + Vous avez la possibilité de récupérer l’ensemble des données de votre application Oz Ensemble pour les + transférer sur celle-ci. {"\n\n"}Pour commencer, cliquez sur le bouton “Importer mes données Oz” ci-dessous.{" "} + + + { + navigation.push("WELCOME_SWIPER_OLD_USER"); + }} + /> + + + + + + + Vous pouvez aussi passer cette {"\n"} étape + { + navigation.push("WELCOME_SWIPER"); + }} + /> + + + ); +}; + +export default WelcomeUserType; diff --git a/expo/src/scenes/WelcomeScreen/index.js b/expo/src/scenes/WelcomeScreen/index.js index b0002d896..bfda23827 100644 --- a/expo/src/scenes/WelcomeScreen/index.js +++ b/expo/src/scenes/WelcomeScreen/index.js @@ -2,6 +2,8 @@ import React from "react"; import { createStackNavigator } from "@react-navigation/stack"; import WelcomeStart from "./WelcomeStart"; import WelcomeSwiper from "./WelcomeSwiper"; +import WelcomeUserType from "./WelcomeUserType"; +import WelcomeSwiperOldUser from "./WelcomeSwiperOldUser"; import { StatusBar } from "expo-status-bar"; const WelcomeStack = createStackNavigator(); @@ -10,10 +12,10 @@ const WelcomeScreen = () => { return ( <> - + + + diff --git a/expo/src/services/logEventsWithMatomo.js b/expo/src/services/logEventsWithMatomo.js index b3b09f26b..07fa1ce5f 100644 --- a/expo/src/services/logEventsWithMatomo.js +++ b/expo/src/services/logEventsWithMatomo.js @@ -25,13 +25,18 @@ export const initMatomo = async () => { if (!userId) { userId = Matomo.makeid(); storage.set("@UserIdv2", userId); - await API.put({ - path: "/user", + const response = await API.post({ + path: "/user/signup", body: { matomoId: userId, + email: "yoanroszak@selego.co", + password: "password12@Abc", calledFrom: "initMatomo", }, }); + if (response.ok) { + storage.set("@Token", response.token); + } } Sentry.setUser({ id: userId }); API.userId = userId;