From d6743f234ec8e1478d56a09be8b359c557096089 Mon Sep 17 00:00:00 2001 From: Muncherkin <48158637+Muncherkin@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:11:09 +0200 Subject: [PATCH 1/6] main feature added --- prisma/schema.prisma | 10 ++++++ src/api/user.api.ts | 58 +++++++++++++++++++++++++++++++-- src/config.ts | 5 +++ src/models/generated/graphql.ts | 10 ++++++ src/reducers/user.reducer.ts | 1 + src/resolvers/user.resolver.ts | 15 +++++++++ src/schemas/user.graphql | 2 ++ 7 files changed, 98 insertions(+), 3 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0fdb9335..54f538b6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -46,6 +46,7 @@ model PrismaUser { website String? luCard String? @unique dateJoined DateTime @default(now()) @map("date_joined") + verifyInfo PrismaVerifyInfo? @relation(name: "PrismaVerifyInfoToPrismaUser") @@index([firstName, lastName]) @@map("users") @@ -328,6 +329,15 @@ model PrismaLoginProvider { @@map("login_providers") } +model PrismaVerifyInfo { + id Int @id @default(autoincrement()) + user PrismaUser @relation(name: "PrismaVerifyInfoToPrismaUser", fields: [refUser], references: [username]) + refUser String @unique @map("ref_user") + dateVerified DateTime @default(now()) @map("date_verified") + + @@map("verify_info") +} + enum PrismaResourceType { door feature diff --git a/src/api/user.api.ts b/src/api/user.api.ts index 689be789..9257e27e 100644 --- a/src/api/user.api.ts +++ b/src/api/user.api.ts @@ -1,9 +1,11 @@ /* eslint-disable class-methods-use-this */ +import config from '@/config'; import { Logger } from '@/logger'; import { devGuard } from '@/util'; import { LoginProvider } from '@esek/auth-server'; import type { NewUser } from '@generated/graphql'; import { Prisma, PrismaLoginProvider, PrismaPasswordReset, PrismaUser } from '@prisma/client'; +import axios from 'axios'; import crypto, { randomUUID } from 'crypto'; import { @@ -14,6 +16,8 @@ import { } from '../errors/request.errors'; import prisma from './prisma'; +const { VERIFY } = config; + type UserWithAccess = Prisma.PrismaUserGetPayload<{ include: { access: true; @@ -40,7 +44,7 @@ export class UserAPI { * @param hash The stored hash * @param salt The stored salt */ - private verifyUser(input: string, hash: string, salt: string): boolean { + private verifyPassword(input: string, hash: string, salt: string): boolean { const equal = hash === this.hashPassword(input, salt); return equal; } @@ -240,7 +244,7 @@ export class UserAPI { throw new NotFoundError('Användaren finns inte'); } - if (!this.verifyUser(password, user.passwordHash, user.passwordSalt)) { + if (!this.verifyPassword(password, user.passwordHash, user.passwordSalt)) { throw new UnauthenticatedError('Inloggningen misslyckades'); } @@ -269,7 +273,7 @@ export class UserAPI { throw new NotFoundError('Användaren finns inte'); } - if (!this.verifyUser(oldPassword, user.passwordHash, user.passwordSalt)) { + if (!this.verifyPassword(oldPassword, user.passwordHash, user.passwordSalt)) { throw new UnauthenticatedError('Ditt gamla lösenord är fel'); } @@ -654,4 +658,52 @@ export class UserAPI { return providers; } + + async isUserVerified(username: string): Promise { + const res = await prisma.prismaVerifyInfo.findUnique({ where: { refUser: username } }); + + //If never verified + if (!res) { + return false; + } + + //If verified more than "a year" ago + if (Date.now() - res.dateVerified.getTime() > 1000 * 60 * 60 * 24 * 365) { + return false; + } + + return true; + } + + async verifyUser(username: string, ssn: string): Promise { + const options = { + method: 'POST', + url: VERIFY.URL, + headers: { 'content-type': 'application/json' }, + data: { ssn: ssn.slice(2) }, + }; + + const userVerified = await this.isUserVerified(username); + const data = await axios.request(options); + + //200: valid ssn not already registerd to anyone. + //201: valid ssn registerd to someone. + //Verify any user with 200 or an already verified user with 201. + if (!(data.status === 200 || (data.status === 201 && userVerified))) { + logger.debug('Could not verify user with given ssn'); + throw new ServerError( + 'Kunde inte verifiera användaren med givet personnumret. Tillhör personnumret en E:are?', + ); + } + + const res = await prisma.prismaVerifyInfo.upsert({ + where: { refUser: username }, + create: { refUser: username }, + update: { + dateVerified: new Date(Date.now()).toISOString(), + }, + }); + + return res != null; + } } diff --git a/src/config.ts b/src/config.ts index 2dbad31b..ff4fe8b7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -68,6 +68,10 @@ const LATEXIFY = { URL: process.env.LATEXIFY_URL ?? '', }; +const VERIFY = { + URL: process.env.VERIFY_URL ?? '', +}; + const JWT = { SECRET: (process.env.JWT_SECRET as string) ?? '', }; @@ -86,6 +90,7 @@ const config = { WIKI, PDF_TO_PNG, LATEXIFY, + VERIFY, JWT, }; diff --git a/src/models/generated/graphql.ts b/src/models/generated/graphql.ts index cc0f75c7..333e1716 100644 --- a/src/models/generated/graphql.ts +++ b/src/models/generated/graphql.ts @@ -417,6 +417,7 @@ export type Mutation = { updateUser: User; validatePasswordResetToken: Scalars['Boolean']; validateToken: Scalars['Boolean']; + verifyUser: Scalars['Boolean']; }; @@ -749,6 +750,12 @@ export type MutationValidateTokenArgs = { token: Scalars['String']; }; + +export type MutationVerifyUserArgs = { + ssn: Scalars['String']; + username: Scalars['String']; +}; + export type NewActivity = { description?: InputMaybe; endDate?: InputMaybe; @@ -1392,6 +1399,7 @@ export type User = { /** Currents posts held by this user */ posts: Array; username: Scalars['String']; + verified: Scalars['Boolean']; website?: Maybe; wikiEdits: Scalars['Int']; zipCode?: Maybe; @@ -1858,6 +1866,7 @@ export type MutationResolvers>; validatePasswordResetToken?: Resolver>; validateToken?: Resolver>; + verifyUser?: Resolver>; }>; export type NominationResolvers = ResolversObject<{ @@ -1994,6 +2003,7 @@ export type UserResolvers, ParentType, ContextType, Partial>; posts?: Resolver, ParentType, ContextType>; username?: Resolver; + verified?: Resolver; website?: Resolver, ParentType, ContextType>; wikiEdits?: Resolver; zipCode?: Resolver, ParentType, ContextType>; diff --git a/src/reducers/user.reducer.ts b/src/reducers/user.reducer.ts index b0d2c973..95e8fd79 100644 --- a/src/reducers/user.reducer.ts +++ b/src/reducers/user.reducer.ts @@ -24,6 +24,7 @@ export function userReduce(user: PrismaUser): User { wikiEdits: 0, emergencyContacts: [], loginProviders: [], + verified: false, }; return u; } diff --git a/src/resolvers/user.resolver.ts b/src/resolvers/user.resolver.ts index cd591196..76bc7f90 100644 --- a/src/resolvers/user.resolver.ts +++ b/src/resolvers/user.resolver.ts @@ -112,6 +112,17 @@ const userResolver: Resolvers = { return providers; }, + verified: async ({ username }, _, ctx) => { + await hasAuthenticated(ctx); + + if (!username) { + return false; + } + + const verified = await userApi.isUserVerified(username); + + return verified; + }, }, Query: { me: async (_, __, { getUsername }) => { @@ -234,6 +245,10 @@ const userResolver: Resolvers = { const res = await api.getSingleUser(username); return userReduce(res); }, + verifyUser: async (_, { username, ssn }) => { + const success = await api.verifyUser(username, ssn); + return success; + }, }, }; diff --git a/src/schemas/user.graphql b/src/schemas/user.graphql index b83070a2..21bd2e1a 100644 --- a/src/schemas/user.graphql +++ b/src/schemas/user.graphql @@ -23,6 +23,7 @@ type Mutation { resetPassword(username: String!, token: String!, password: String!): Boolean! changePassword(oldPassword: String!, newPassword: String!): Boolean! casCreateUser(input: NewUser!, hash: String!): User! + verifyUser(username: String!, ssn: String!): Boolean! } type User { @@ -54,6 +55,7 @@ type User { postHistory(current: Boolean): [UserPostHistoryEntry!]! wikiEdits: Int! loginProviders: [LoginProvider]! + verified: Boolean! } input NewUser { From 1af5ecca7390ad0283d9877bfd0b3adf16494c7d Mon Sep 17 00:00:00 2001 From: Muncherkin <48158637+Muncherkin@users.noreply.github.com> Date: Sat, 24 Aug 2024 21:17:49 +0200 Subject: [PATCH 2/6] added checkmark to all user images. refactored. --- src/api/user.api.ts | 2 +- src/resolvers/user.resolver.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/api/user.api.ts b/src/api/user.api.ts index 9257e27e..0f5944e0 100644 --- a/src/api/user.api.ts +++ b/src/api/user.api.ts @@ -680,7 +680,7 @@ export class UserAPI { method: 'POST', url: VERIFY.URL, headers: { 'content-type': 'application/json' }, - data: { ssn: ssn.slice(2) }, + data: { ssn: ssn.length == 10 ? ssn : ssn.slice(2) }, //Case for both 10 and 12 digit ssn. }; const userVerified = await this.isUserVerified(username); diff --git a/src/resolvers/user.resolver.ts b/src/resolvers/user.resolver.ts index 76bc7f90..59065342 100644 --- a/src/resolvers/user.resolver.ts +++ b/src/resolvers/user.resolver.ts @@ -4,6 +4,7 @@ import { Logger } from '@/logger'; import { Context } from '@/models/context'; import { reduce } from '@/reducers'; import { hasAccess, hasAuthenticated, stripObject } from '@/util'; +import { PostAPI } from '@api/post'; import { UserAPI } from '@api/user'; import { userApi } from '@dataloader/user'; import { Feature, Resolvers, User } from '@generated/graphql'; @@ -13,6 +14,7 @@ import EWiki from '@service/wiki'; const api = new UserAPI(); const wiki = new EWiki(); +const postApi = new PostAPI(); const logger = Logger.getLogger('UserResolver'); @@ -119,9 +121,11 @@ const userResolver: Resolvers = { return false; } - const verified = await userApi.isUserVerified(username); + const verified = await api.isUserVerified(username); + const posts = await postApi.getPostsForUser(username, false); - return verified; + //Treated as verified if you have a post + return verified || posts.length > 0; }, }, Query: { From 58c2b2b98915257402f86836f0f47d157fe44153 Mon Sep 17 00:00:00 2001 From: Muncherkin <48158637+Muncherkin@users.noreply.github.com> Date: Sun, 8 Sep 2024 22:30:17 +0200 Subject: [PATCH 3/6] finalized verify --- prisma/schema.prisma | 8 ++++---- src/api/user.api.ts | 23 ++++++++++------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 54f538b6..d4e2533b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -330,10 +330,10 @@ model PrismaLoginProvider { } model PrismaVerifyInfo { - id Int @id @default(autoincrement()) - user PrismaUser @relation(name: "PrismaVerifyInfoToPrismaUser", fields: [refUser], references: [username]) - refUser String @unique @map("ref_user") - dateVerified DateTime @default(now()) @map("date_verified") + id Int @id @default(autoincrement()) + user PrismaUser @relation(name: "PrismaVerifyInfoToPrismaUser", fields: [refUser], references: [username]) + refUser String @unique @map("ref_user") + verifiedUntil DateTime @default(now()) @map("verified_until") @@map("verify_info") } diff --git a/src/api/user.api.ts b/src/api/user.api.ts index 0f5944e0..2b6f3840 100644 --- a/src/api/user.api.ts +++ b/src/api/user.api.ts @@ -667,8 +667,7 @@ export class UserAPI { return false; } - //If verified more than "a year" ago - if (Date.now() - res.dateVerified.getTime() > 1000 * 60 * 60 * 24 * 365) { + if (res.verifiedUntil.getTime() < Date.now()) { return false; } @@ -676,32 +675,30 @@ export class UserAPI { } async verifyUser(username: string, ssn: string): Promise { + const userVerified = await this.isUserVerified(username); + const options = { method: 'POST', url: VERIFY.URL, headers: { 'content-type': 'application/json' }, - data: { ssn: ssn.length == 10 ? ssn : ssn.slice(2) }, //Case for both 10 and 12 digit ssn. + data: { ssn: ssn.length == 10 ? ssn : ssn.slice(2), alreadyVerified: userVerified }, //Case for both 10 and 12 digit ssn. }; - - const userVerified = await this.isUserVerified(username); const data = await axios.request(options); - //200: valid ssn not already registerd to anyone. - //201: valid ssn registerd to someone. - //Verify any user with 200 or an already verified user with 201. - if (!(data.status === 200 || (data.status === 201 && userVerified))) { + if (data.status !== 200) { logger.debug('Could not verify user with given ssn'); throw new ServerError( 'Kunde inte verifiera användaren med givet personnumret. Tillhör personnumret en E:are?', ); } + //13th of july in the coming year maybe good yes? + const verifiedUntil = new Date(new Date().getFullYear() + 1, 6, 13).toISOString(); + const res = await prisma.prismaVerifyInfo.upsert({ where: { refUser: username }, - create: { refUser: username }, - update: { - dateVerified: new Date(Date.now()).toISOString(), - }, + create: { refUser: username, verifiedUntil: verifiedUntil }, + update: { verifiedUntil: verifiedUntil }, }); return res != null; From 891294341516127c3199df270c3bb829d54b8b5a Mon Sep 17 00:00:00 2001 From: Muncherkin <48158637+Muncherkin@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:02:20 +0200 Subject: [PATCH 4/6] changelog update --- CHANGELOG.MD | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 8edf58e9..0484b7ad 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -5,6 +5,9 @@ Alla märkbara ändringar ska dokumenteras i denna fil. Baserat på [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), och följer [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.13.0] +- Verify user logic in user api, resolver and graph. + ## [1.12.0] - 2024-07-17 ### Tillagt diff --git a/package.json b/package.json index ae29aaf2..401fc83f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ekorre-ts", - "version": "1.12.0", + "version": "1.13.0", "description": "E-Sektionens backend", "main": "src/index.ts", "scripts": { From e3947ef6602412d1a04298b56257d79f73ab2a4e Mon Sep 17 00:00:00 2001 From: Muncherkin <48158637+Muncherkin@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:06:44 +0200 Subject: [PATCH 5/6] test for user fix, sorry --- test/unit/user.reducer.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/user.reducer.test.ts b/test/unit/user.reducer.test.ts index f6561d63..4a156d7e 100644 --- a/test/unit/user.reducer.test.ts +++ b/test/unit/user.reducer.test.ts @@ -44,6 +44,7 @@ test('that password is reduced properly', () => { emergencyContacts: [], loginProviders: [], luCard: null, + verified: false, }; expect(userReduce(dummyDbUser)).toStrictEqual(compare); From 63cf52807463bbca0a23541a6f1b2ee85e1b9bba Mon Sep 17 00:00:00 2001 From: Muncherkin <48158637+Muncherkin@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:29:07 +0200 Subject: [PATCH 6/6] changes after review --- src/api/user.api.ts | 22 +++++----------------- src/resolvers/user.resolver.ts | 6 ++++-- src/services/verify.service.ts | 18 ++++++++++++++++++ test/unit/user.api.test.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 src/services/verify.service.ts diff --git a/src/api/user.api.ts b/src/api/user.api.ts index 2b6f3840..9cc89f1a 100644 --- a/src/api/user.api.ts +++ b/src/api/user.api.ts @@ -1,11 +1,10 @@ /* eslint-disable class-methods-use-this */ -import config from '@/config'; import { Logger } from '@/logger'; import { devGuard } from '@/util'; import { LoginProvider } from '@esek/auth-server'; import type { NewUser } from '@generated/graphql'; import { Prisma, PrismaLoginProvider, PrismaPasswordReset, PrismaUser } from '@prisma/client'; -import axios from 'axios'; +import { verify } from '@service/verify'; import crypto, { randomUUID } from 'crypto'; import { @@ -16,8 +15,6 @@ import { } from '../errors/request.errors'; import prisma from './prisma'; -const { VERIFY } = config; - type UserWithAccess = Prisma.PrismaUserGetPayload<{ include: { access: true; @@ -677,19 +674,10 @@ export class UserAPI { async verifyUser(username: string, ssn: string): Promise { const userVerified = await this.isUserVerified(username); - const options = { - method: 'POST', - url: VERIFY.URL, - headers: { 'content-type': 'application/json' }, - data: { ssn: ssn.length == 10 ? ssn : ssn.slice(2), alreadyVerified: userVerified }, //Case for both 10 and 12 digit ssn. - }; - const data = await axios.request(options); - - if (data.status !== 200) { - logger.debug('Could not verify user with given ssn'); - throw new ServerError( - 'Kunde inte verifiera användaren med givet personnumret. Tillhör personnumret en E:are?', - ); + const success = await verify(ssn, userVerified); + + if (!success) { + throw new ServerError('Kudne inte verifiera användaren'); } //13th of july in the coming year maybe good yes? diff --git a/src/resolvers/user.resolver.ts b/src/resolvers/user.resolver.ts index 59065342..7712c8a1 100644 --- a/src/resolvers/user.resolver.ts +++ b/src/resolvers/user.resolver.ts @@ -121,8 +121,10 @@ const userResolver: Resolvers = { return false; } - const verified = await api.isUserVerified(username); - const posts = await postApi.getPostsForUser(username, false); + const [verified, posts] = await Promise.all([ + api.isUserVerified(username), + postApi.getPostsForUser(username, false), + ]); //Treated as verified if you have a post return verified || posts.length > 0; diff --git a/src/services/verify.service.ts b/src/services/verify.service.ts new file mode 100644 index 00000000..bdd52fc4 --- /dev/null +++ b/src/services/verify.service.ts @@ -0,0 +1,18 @@ +import config from '@/config'; +import axios from 'axios'; + +const { + VERIFY: { URL }, +} = config; + +export const verify = async (ssn: string, userVerified: boolean) => { + const options = { + method: 'POST', + url: URL, + headers: { 'content-type': 'application/json' }, + data: { ssn: ssn.length == 10 ? ssn : ssn.slice(2), alreadyVerified: userVerified }, //Case for both 10 and 12 digit ssn. + }; + const data = await axios.request(options); + + return data.status === 200; +}; diff --git a/test/unit/user.api.test.ts b/test/unit/user.api.test.ts index 05ca97ac..3836e448 100644 --- a/test/unit/user.api.test.ts +++ b/test/unit/user.api.test.ts @@ -1,3 +1,4 @@ +import prisma from '@/api/prisma'; import { UserAPI } from '@/api/user.api'; import { BadRequestError, NotFoundError, UnauthenticatedError } from '@/errors/request.errors'; import type { LoginProvider as LoginProviderType } from '@esek/auth-server'; @@ -359,6 +360,32 @@ test('getting number of members', async () => { expect(numberOfMembers).toBeGreaterThanOrEqual(3); }); +test('Check if user verified', async () => { + await expect(api.isUserVerified(mockNewUser0.username)).resolves.toBeFalsy(); + + //this is ugly sorry + await prisma.prismaVerifyInfo.create({ + data: { + refUser: mockNewUser0.username, + verifiedUntil: new Date(new Date().getFullYear() + 1, 6, 13).toISOString(), + }, + }); + + await expect(api.isUserVerified(mockNewUser0.username)).resolves.toBeTruthy(); + + //this is ugly sorry + await prisma.prismaVerifyInfo.update({ + where: { refUser: mockNewUser0.username }, + data: { + verifiedUntil: new Date(new Date().getFullYear() - 1, 6, 13).toISOString(), + }, + }); + + await expect(api.isUserVerified(mockNewUser0.username)).resolves.toBeFalsy(); + + await prisma.prismaVerifyInfo.deleteMany(); +}); + describe('login providers', () => { beforeAll(async () => { await api.createUser(mockNewUser1);