diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 78c21f34..608301ca 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.1] - 2024-09-20 ### Tillagt diff --git a/package.json b/package.json index da54f0e1..401fc83f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ekorre-ts", - "version": "1.12.1", + "version": "1.13.0", "description": "E-Sektionens backend", "main": "src/index.ts", "scripts": { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0fdb9335..d4e2533b 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") + verifiedUntil DateTime @default(now()) @map("verified_until") + + @@map("verify_info") +} + enum PrismaResourceType { door feature diff --git a/src/api/user.api.ts b/src/api/user.api.ts index 689be789..9cc89f1a 100644 --- a/src/api/user.api.ts +++ b/src/api/user.api.ts @@ -4,6 +4,7 @@ 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 { verify } from '@service/verify'; import crypto, { randomUUID } from 'crypto'; import { @@ -40,7 +41,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 +241,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 +270,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 +655,40 @@ 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 (res.verifiedUntil.getTime() < Date.now()) { + return false; + } + + return true; + } + + async verifyUser(username: string, ssn: string): Promise { + const userVerified = await this.isUserVerified(username); + + 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? + const verifiedUntil = new Date(new Date().getFullYear() + 1, 6, 13).toISOString(); + + const res = await prisma.prismaVerifyInfo.upsert({ + where: { refUser: username }, + create: { refUser: username, verifiedUntil: verifiedUntil }, + update: { verifiedUntil: verifiedUntil }, + }); + + 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..7712c8a1 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'); @@ -112,6 +114,21 @@ const userResolver: Resolvers = { return providers; }, + verified: async ({ username }, _, ctx) => { + await hasAuthenticated(ctx); + + if (!username) { + return 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; + }, }, Query: { me: async (_, __, { getUsername }) => { @@ -234,6 +251,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 { 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); 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);