├── frontend
├── src
│ ├── assets
│ │ ├── .gitkeep
│ │ ├── cover.jpg
│ │ ├── heart.png
│ │ ├── image.jpg
│ │ ├── image.png
│ │ └── report.pdf
│ ├── app
│ │ ├── containers
│ │ │ ├── about
│ │ │ │ ├── about.container.scss
│ │ │ │ ├── index.ts
│ │ │ │ ├── about.container.html
│ │ │ │ └── about.container.ts
│ │ │ ├── howto
│ │ │ │ ├── howto.container.scss
│ │ │ │ ├── index.ts
│ │ │ │ ├── howto.container.html
│ │ │ │ └── howto.container.ts
│ │ │ ├── login
│ │ │ │ ├── login.container.scss
│ │ │ │ ├── index.ts
│ │ │ │ ├── login.container.html
│ │ │ │ └── login.container.ts
│ │ │ ├── signup
│ │ │ │ ├── signup.component.scss
│ │ │ │ ├── index.ts
│ │ │ │ ├── signup.component.html
│ │ │ │ └── signup.component.ts
│ │ │ ├── home
│ │ │ │ ├── index.ts
│ │ │ │ └── home.component.ts
│ │ │ ├── main
│ │ │ │ ├── index.ts
│ │ │ │ ├── main.component.scss
│ │ │ │ ├── main.component.html
│ │ │ │ └── main.component.ts
│ │ │ ├── how-it-works
│ │ │ │ ├── index.ts
│ │ │ │ ├── how-it-works.component.scss
│ │ │ │ ├── how-it-works.component.html
│ │ │ │ └── how-it-works.component.ts
│ │ │ └── index.ts
│ │ ├── components
│ │ │ ├── about
│ │ │ │ ├── index.ts
│ │ │ │ ├── about.component.scss
│ │ │ │ ├── about.component.ts
│ │ │ │ └── about.component.html
│ │ │ ├── home
│ │ │ │ ├── index.ts
│ │ │ │ ├── home.component.scss
│ │ │ │ ├── home.component.ts
│ │ │ │ └── home.component.html
│ │ │ ├── howto
│ │ │ │ ├── index.ts
│ │ │ │ ├── howto.component.scss
│ │ │ │ ├── howto.component.ts
│ │ │ │ └── howto.component.html
│ │ │ ├── login
│ │ │ │ ├── index.ts
│ │ │ │ ├── login.component.scss
│ │ │ │ ├── login.component.ts
│ │ │ │ └── login.component.html
│ │ │ ├── search
│ │ │ │ ├── index.ts
│ │ │ │ ├── search.component.html
│ │ │ │ ├── search.component.css
│ │ │ │ └── search.component.ts
│ │ │ ├── signup
│ │ │ │ ├── index.ts
│ │ │ │ ├── signup.component.scss
│ │ │ │ ├── signup.component.html
│ │ │ │ └── signup.component.ts
│ │ │ ├── results
│ │ │ │ ├── index.ts
│ │ │ │ ├── results.component.scss
│ │ │ │ ├── results.component.ts
│ │ │ │ └── results.component.html
│ │ │ ├── student
│ │ │ │ ├── index.ts
│ │ │ │ ├── student.component.html
│ │ │ │ ├── student.component.css
│ │ │ │ └── student.component.ts
│ │ │ ├── countdown
│ │ │ │ ├── index.ts
│ │ │ │ ├── countdown.component.scss
│ │ │ │ ├── countdown.component.html
│ │ │ │ └── countdown.component.ts
│ │ │ └── index.ts
│ │ ├── app.component.scss
│ │ ├── app.component.ts
│ │ ├── app.component.html
│ │ ├── guards
│ │ │ ├── logged-in.guard.ts
│ │ │ └── logged-out.guard.ts
│ │ ├── app-routing.module.ts
│ │ ├── app.component.spec.ts
│ │ ├── app.module.ts
│ │ ├── crypto.ts
│ │ └── services
│ │ │ └── main.service.ts
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── favicon.ico
│ ├── styles.scss
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ ├── main.ts
│ ├── index.html
│ ├── ngsw-config.json
│ ├── test.ts
│ └── polyfills.ts
├── .dockerignore
├── proxy.conf.json
├── e2e
│ ├── app.po.ts
│ ├── tsconfig.e2e.json
│ └── app.e2e-spec.ts
├── .editorconfig
├── nginx.conf
├── tsconfig.json
├── Dockerfile
├── .gitignore
├── protractor.conf.js
├── karma.conf.js
├── README.md
├── .angular-cli.json
├── package.json
└── tslint.json
├── service
├── results
│ ├── .gitignore
│ ├── .dockerignore
│ ├── mongo.go
│ ├── Gopkg.lock
│ ├── config.go
│ ├── Gopkg.toml
│ ├── Dockerfile
│ ├── user.go
│ └── results.go
├── signup
│ ├── .dockerignore
│ ├── .gitignore
│ ├── mongo.go
│ ├── Gopkg.lock
│ ├── Gopkg.toml
│ ├── Dockerfile
│ ├── config.go
│ ├── signup.go
│ ├── user.go
│ └── routines.go
└── nginx
│ ├── Dockerfile
│ └── nginx.prod.conf
├── cover.jpg
├── report
├── .gitignore
├── puppylove.pdf
└── puppylove.tex
├── .dockerignore
├── scripts
├── mongodump
│ └── puppy
│ │ ├── user.bson
│ │ ├── declare.bson
│ │ ├── heart.bson
│ │ ├── heart.metadata.json
│ │ ├── user.metadata.json
│ │ └── declare.metadata.json
├── compute.sh
├── logsof.sh
├── smallcompute.sh
├── newuser.sh
├── login.sh
├── script.js
├── allusers.py
└── package-lock.json
├── .gitignore
├── .backend.env
├── models
├── hearts.go
├── declare.go
└── user.go
├── db
└── mongo.go
├── utils
├── http.go
└── utils.go
├── Gopkg.toml
├── Dockerfile
├── docker-compose.yml
├── main.go
├── LICENSE
├── puppy.nginx.conf
├── controllers
├── declare.go
├── heart.go
├── list.go
├── session.go
├── stats.go
└── user.go
├── config
└── config.go
├── router
└── router.go
├── Gopkg.lock
└── README.md
/frontend/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/service/results/.gitignore:
--------------------------------------------------------------------------------
1 | /results
2 | vendor/
3 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/about/about.container.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/howto/howto.container.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/login/login.container.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/signup/signup.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/service/results/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | vendor
3 |
--------------------------------------------------------------------------------
/service/signup/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | vendor
3 |
--------------------------------------------------------------------------------
/service/signup/.gitignore:
--------------------------------------------------------------------------------
1 | signup
2 | vendor/
3 | .envfile
4 |
--------------------------------------------------------------------------------
/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/cover.jpg
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .dockerignore
3 | node_modules
4 | dist
--------------------------------------------------------------------------------
/report/.gitignore:
--------------------------------------------------------------------------------
1 | *.aux
2 | *.log
3 | *.out
4 | *.ps
5 | /auto
6 | /auto/*
7 |
--------------------------------------------------------------------------------
/frontend/src/app/components/about/index.ts:
--------------------------------------------------------------------------------
1 | export * from './about.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/components/home/index.ts:
--------------------------------------------------------------------------------
1 | export * from './home.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/components/howto/index.ts:
--------------------------------------------------------------------------------
1 | export * from './howto.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/components/login/index.ts:
--------------------------------------------------------------------------------
1 | export * from './login.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/components/search/index.ts:
--------------------------------------------------------------------------------
1 | export * from './search.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/components/signup/index.ts:
--------------------------------------------------------------------------------
1 | export * from './signup.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/about/index.ts:
--------------------------------------------------------------------------------
1 | export * from './about.container';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/home/index.ts:
--------------------------------------------------------------------------------
1 | export * from './home.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/howto/index.ts:
--------------------------------------------------------------------------------
1 | export * from './howto.container';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/login/index.ts:
--------------------------------------------------------------------------------
1 | export * from './login.container';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/main/index.ts:
--------------------------------------------------------------------------------
1 | export * from './main.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/signup/index.ts:
--------------------------------------------------------------------------------
1 | export * from './signup.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/components/results/index.ts:
--------------------------------------------------------------------------------
1 | export * from './results.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/components/student/index.ts:
--------------------------------------------------------------------------------
1 | export * from './student.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/about/about.container.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/howto/howto.container.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/app/components/countdown/index.ts:
--------------------------------------------------------------------------------
1 | export * from './countdown.component';
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/how-it-works/index.ts:
--------------------------------------------------------------------------------
1 | export * from './how-it-works.component';
2 |
--------------------------------------------------------------------------------
/report/puppylove.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/report/puppylove.pdf
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | frontend/
2 | vendor/
3 | Dockerfile
4 | .dockerignore
5 | cover.jpg
6 | service/
7 |
--------------------------------------------------------------------------------
/frontend/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/frontend/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/favicon.ico
--------------------------------------------------------------------------------
/frontend/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/how-it-works/how-it-works.component.scss:
--------------------------------------------------------------------------------
1 | [mat-button] {
2 | width: 100%;
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/cover.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/heart.png
--------------------------------------------------------------------------------
/frontend/src/assets/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/image.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/image.png
--------------------------------------------------------------------------------
/frontend/src/assets/report.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/frontend/src/assets/report.pdf
--------------------------------------------------------------------------------
/scripts/mongodump/puppy/user.bson:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/scripts/mongodump/puppy/user.bson
--------------------------------------------------------------------------------
/scripts/compute.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | http -v --timeout=24000 'localhost:3000/admin/compute/prepare/1' "$CADMIN"
4 |
--------------------------------------------------------------------------------
/scripts/mongodump/puppy/declare.bson:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/scripts/mongodump/puppy/declare.bson
--------------------------------------------------------------------------------
/scripts/mongodump/puppy/heart.bson:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pclubiitk/puppy-love/HEAD/scripts/mongodump/puppy/heart.bson
--------------------------------------------------------------------------------
/frontend/src/app/components/about/about.component.scss:
--------------------------------------------------------------------------------
1 | [mat-button] {
2 | width: 100%;
3 | }
4 |
5 | li {
6 | margin: 5px auto;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/app/components/howto/howto.component.scss:
--------------------------------------------------------------------------------
1 | [mat-button] {
2 | width: 100%;
3 | }
4 |
5 | li {
6 | margin: 5px auto;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/main/main.component.scss:
--------------------------------------------------------------------------------
1 | mat-card {
2 | margin: 0 auto;
3 | max-width: 90%;
4 | width: 500px;
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/scripts/logsof.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | LOGFILE=$(docker inspect --format='{{.LogPath}}' $1)
4 | APP=${2:-vim}
5 | $APP $LOGFILE
6 |
--------------------------------------------------------------------------------
/frontend/src/app/components/login/login.component.scss:
--------------------------------------------------------------------------------
1 | mat-form-field {
2 | width: 100%;
3 | }
4 |
5 | [mat-button] {
6 | width: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/src/app/components/signup/signup.component.scss:
--------------------------------------------------------------------------------
1 | mat-form-field {
2 | width: 100%;
3 | }
4 |
5 | [mat-button] {
6 | width: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/proxy.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api": {
3 | "target": "http://localhost:3000/",
4 | "pathRewrite": {
5 | "^/api" : ""
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm
3 | .#*
4 | npm-debug.log
5 | .tern-port
6 | puppy-love
7 | vendor
8 | views/build
9 | .mail.env
10 | .pass.env
11 | /res
12 |
--------------------------------------------------------------------------------
/frontend/src/app/components/countdown/countdown.component.scss:
--------------------------------------------------------------------------------
1 | [mat-button] {
2 | width: 100%;
3 | }
4 |
5 | .mat-display-1 {
6 | text-align: center;
7 | }
8 |
--------------------------------------------------------------------------------
/scripts/mongodump/puppy/heart.metadata.json:
--------------------------------------------------------------------------------
1 | {"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"puppy.heart"}],"uuid":"da3618e0c81640cea36b871c96496718"}
--------------------------------------------------------------------------------
/scripts/mongodump/puppy/user.metadata.json:
--------------------------------------------------------------------------------
1 | {"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"puppy.user"}],"uuid":"e42ccd5dc0274dcc811d2f1c324271eb"}
--------------------------------------------------------------------------------
/scripts/mongodump/puppy/declare.metadata.json:
--------------------------------------------------------------------------------
1 | {"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"puppy.declare"}],"uuid":"4643a53cc4fd43c999e25f584e81df08"}
--------------------------------------------------------------------------------
/service/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | # Set nginx base image
2 | FROM nginx
3 |
4 | # Copy custom configuration file from the current directory
5 | COPY nginx.prod.conf /etc/nginx/nginx.conf
6 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/main/main.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/scripts/smallcompute.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | read -p "Roll: " id
4 | read -p "Required gender: " req
5 |
6 | http -v --timeout=24000 "localhost:3000/admin/compute/preparesmall/${id}/${req}" "$CADMIN"
7 |
--------------------------------------------------------------------------------
/.backend.env:
--------------------------------------------------------------------------------
1 | SIGNUP_PORT_3001_TCP_PORT=3001
2 | SIGNUP_PORT_3001_TCP_ADDR=signup
3 |
4 | MONGO_PORT_27017_TCP_PORT=27017
5 | MONGO_PORT_27017_TCP_ADDR=mongo
6 |
7 | TABLE=declare
8 |
9 | GIN_MODE=release
10 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/how-it-works/how-it-works.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Back
4 |
5 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/login/login.container.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | template: `
5 |
6 | `,
7 | styles: [ '' ]
8 | })
9 | export class HomeComponent {
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/main/main.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | templateUrl: './main.component.html',
5 | styleUrls: [ './main.component.scss' ]
6 | })
7 | export class MainComponent {
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/about/about.container.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | templateUrl: './about.container.html',
5 | styleUrls: [ './about.container.scss' ]
6 | })
7 | export class AboutComponent {
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/howto/howto.container.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | templateUrl: './howto.container.html',
5 | styleUrls: [ './howto.container.scss' ]
6 | })
7 | export class HowToComponent {
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('puppy-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/how-it-works/how-it-works.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | templateUrl: './how-it-works.component.html',
5 | styleUrls: [ './how-it-works.component.scss' ]
6 | })
7 | export class HowItWorksComponent {
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/signup/signup.component.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/frontend/src/app/components/about/about.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'puppy-about',
5 | templateUrl: './about.component.html',
6 | styleUrls: [ './about.component.scss' ]
7 | })
8 | export class AboutComponent {
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/app/components/howto/howto.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'puppy-howto',
5 | templateUrl: './howto.component.html',
6 | styleUrls: [ './howto.component.scss' ]
7 | })
8 | export class HowToComponent {
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "module": "es2015",
7 | "types": []
8 | },
9 | "exclude": [
10 | "test.ts",
11 | "**/*.spec.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/frontend/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "jasminewd2",
11 | "node"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/nginx.conf:
--------------------------------------------------------------------------------
1 | worker_processes 4;
2 |
3 | events { worker_connections 1024; }
4 |
5 | http {
6 | server {
7 | include /etc/nginx/mime.types;
8 | listen 80;
9 | location / {
10 | index index.html;
11 | root /src/dist;
12 | try_files $uri /index.html;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/scripts/newuser.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | read -p "Roll: " roll
4 | read -p "Name: " name
5 | read -p "Gender (0/1): " gender
6 | read -p "Image: " image
7 | read -p "Email: " email
8 |
9 | http -v 'pclub.cse.iitk.ac.in/api/admin/user/new' roll="$roll" name="$name" email="$email" gender="$gender" image="$image" passHash="aaaa" "$CADMIN"
10 |
--------------------------------------------------------------------------------
/frontend/src/app/components/student/student.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ student.name }}
6 | {{ student._id }}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/frontend/src/app/components/countdown/countdown.component.html:
--------------------------------------------------------------------------------
1 |
2 | Time until portal is active:
3 | {{ current }}
4 | How It Works
5 | About
6 | How To Use
7 |
8 |
--------------------------------------------------------------------------------
/frontend/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 |
3 | describe('frontend App', () => {
4 | let page: AppPage;
5 |
6 | beforeEach(() => {
7 | page = new AppPage();
8 | });
9 |
10 | it('should display welcome message', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('Welcome to puppy!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/frontend/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "types": [
8 | "jasmine",
9 | "node"
10 | ]
11 | },
12 | "files": [
13 | "test.ts"
14 | ],
15 | "include": [
16 | "**/*.spec.ts",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/scripts/login.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo "Username: ${1}"
4 | VAL=$(http 'pclub.cse.iitk.ac.in/api/session/login' username="$1" password="$2" --header | tail -n 2 | head -n1)
5 | echo "Cookie is ${VAL}"
6 |
7 | if [ "$1" = "admin" ];
8 | then
9 | echo "Logging in as admin"
10 | export CADMIN="Cookie:${VAL:12}"
11 | else
12 | echo "Logging in as ${1}"
13 | export COOKIE="Cookie:${VAL:12}"
14 | fi;
15 |
--------------------------------------------------------------------------------
/frontend/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/frontend/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | p {
2 | font-size: 35%;
3 | margin-top: 0px;
4 | margin-bottom: 0px;
5 | padding-top: 0px;
6 | padding-bottom: 0px;
7 | }
8 |
9 | h4 {
10 | margin-bottom: 0px;
11 | padding-bottom: 0px;
12 | }
13 |
14 | img {
15 | margin-top: 0px;
16 | margin-bottom: 0px;
17 | padding-top: 0px;
18 | padding-bottom: 0px;
19 | }
20 |
21 | h1 {
22 | margin-bottom: 20px;
23 | padding-bottom: 0px;
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/frontend/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { MatSnackBar } from '@angular/material/snack-bar';
3 |
4 | @Component({
5 | selector: 'puppy-root',
6 | templateUrl: './app.component.html',
7 | styleUrls: ['./app.component.scss']
8 | })
9 | export class AppComponent implements OnInit {
10 |
11 | constructor(
12 | public snackBar: MatSnackBar,
13 | ) {}
14 |
15 | ngOnInit() {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/main.ts:
--------------------------------------------------------------------------------
1 | import 'hammerjs';
2 |
3 | import { enableProdMode } from '@angular/core';
4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
5 |
6 | import { AppModule } from './app/app.module';
7 | import { environment } from './environments/environment';
8 |
9 | if (environment.production) {
10 | enableProdMode();
11 | }
12 |
13 | platformBrowserDynamic().bootstrapModule(AppModule)
14 | .catch(err => console.log(err));
15 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es5",
11 | "typeRoots": [
12 | "node_modules/@types"
13 | ],
14 | "lib": [
15 | "es2017",
16 | "dom"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # pclubiitk/puppy-love:frontend
2 | FROM node:8-alpine as builder
3 |
4 | # Cached layer for node_modules
5 | COPY package.json /tmp/package.json
6 | COPY yarn.lock /tmp/yarn.lock
7 | RUN cd /tmp && yarn install
8 | RUN mkdir -p /src && cp -a /tmp/node_modules /src/
9 |
10 | WORKDIR /src
11 | COPY . .
12 | RUN yarn build
13 |
14 | FROM nginx
15 |
16 | RUN mkdir -p /src/dist
17 | COPY --from=builder /src/dist /src/dist
18 | COPY nginx.conf /etc/nginx/nginx.conf
19 |
--------------------------------------------------------------------------------
/frontend/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Puppy Love @iitk!
5 | Programming Club IIT Kanpur
6 | in association with
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/service/results/mongo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "gopkg.in/mgo.v2"
5 | )
6 |
7 | type PuppyDb struct {
8 | S *mgo.Session
9 | }
10 |
11 | func MongoConnect() (PuppyDb, error) {
12 | S, err := mgo.Dial(CfgMgoUrl)
13 | return PuppyDb{S}, err
14 | }
15 |
16 | func (db PuppyDb) GetById(table string, id string) *mgo.Query {
17 | return db.S.DB("puppy").C(table).FindId(id)
18 | }
19 |
20 | func (db PuppyDb) GetCollection(table string) *mgo.Collection {
21 | return db.S.DB("puppy").C(table)
22 | }
23 |
--------------------------------------------------------------------------------
/service/signup/mongo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "gopkg.in/mgo.v2"
5 | )
6 |
7 | type PuppyDb struct {
8 | S *mgo.Session
9 | }
10 |
11 | func MongoConnect() (PuppyDb, error) {
12 | S, err := mgo.Dial(CfgMgoUrl)
13 | return PuppyDb{S}, err
14 | }
15 |
16 | func (db PuppyDb) GetById(table string, id string) *mgo.Query {
17 | return db.S.DB("puppy").C(table).FindId(id)
18 | }
19 |
20 | func (db PuppyDb) GetCollection(table string) *mgo.Collection {
21 | return db.S.DB("puppy").C(table)
22 | }
23 |
--------------------------------------------------------------------------------
/models/hearts.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type (
4 | Heart struct {
5 | Id string `json:"roll" bson:"roll"`
6 | Gender string `json:"gender" bson:"gender"`
7 | Time uint64 `json:"time" bson:"time"`
8 | Value string `json:"v" bson:"v"`
9 | Data string `json:"data" bson:"data"`
10 | }
11 |
12 | GotHeart struct {
13 | Value string `json:"v" bson:"v"`
14 | Data string `json:"data" bson:"data"`
15 | GenderOfSender string `json:"genderOfSender" bson:"genderOfSender"`
16 | }
17 | )
18 |
--------------------------------------------------------------------------------
/frontend/src/app/guards/logged-in.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Router, CanActivate } from '@angular/router';
3 |
4 | import { MainService } from '../services/main.service';
5 |
6 | @Injectable()
7 | export class LoggedInGuard implements CanActivate {
8 | constructor(private router: Router,
9 | private main: MainService) {}
10 |
11 | canActivate() {
12 | if (!this.main.loggedIn) {
13 | this.router.navigate([ '/login' ]);
14 | }
15 | return this.main.loggedIn;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/app/guards/logged-out.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Router, CanActivate } from '@angular/router';
3 |
4 | import { MainService } from '../services/main.service';
5 |
6 | @Injectable()
7 | export class LoggedOutGuard implements CanActivate {
8 | constructor(private router: Router,
9 | private main: MainService) {}
10 |
11 | canActivate() {
12 | if (this.main.loggedIn) {
13 | this.router.navigate([ '/home' ]);
14 | }
15 | return !this.main.loggedIn;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/db/mongo.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "github.com/pclubiitk/puppy-love/config"
5 |
6 | "gopkg.in/mgo.v2"
7 | )
8 |
9 | type PuppyDb struct {
10 | S *mgo.Session
11 | }
12 |
13 | func MongoConnect() (PuppyDb, error) {
14 | S, err := mgo.Dial(config.CfgMgoUrl)
15 | return PuppyDb{S}, err
16 | }
17 |
18 | func (db PuppyDb) GetById(table string, id string) *mgo.Query {
19 | return db.S.DB("puppy").C(table).FindId(id)
20 | }
21 |
22 | func (db PuppyDb) GetCollection(table string) *mgo.Collection {
23 | return db.S.DB("puppy").C(table)
24 | }
25 |
--------------------------------------------------------------------------------
/service/results/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | branch = "v2"
6 | name = "gopkg.in/mgo.v2"
7 | packages = [".","bson","internal/json","internal/sasl","internal/scram"]
8 | revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655"
9 |
10 | [solve-meta]
11 | analyzer-name = "dep"
12 | analyzer-version = 1
13 | inputs-digest = "5bc236996eb0c3dd772accbe0b5122e79b1ed74e50984babc0664a2ad68ad18f"
14 | solver-name = "gps-cdcl"
15 | solver-version = 1
16 |
--------------------------------------------------------------------------------
/service/signup/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | branch = "v2"
6 | name = "gopkg.in/mgo.v2"
7 | packages = [".","bson","internal/json","internal/sasl","internal/scram"]
8 | revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655"
9 |
10 | [solve-meta]
11 | analyzer-name = "dep"
12 | analyzer-version = 1
13 | inputs-digest = "5bc236996eb0c3dd772accbe0b5122e79b1ed74e50984babc0664a2ad68ad18f"
14 | solver-name = "gps-cdcl"
15 | solver-version = 1
16 |
--------------------------------------------------------------------------------
/frontend/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Puppy Love
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/service/results/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | )
6 |
7 | var CfgMgoUrl = "mongodb://0.0.0.0:27017/puppy"
8 | var CfgTable = "declare"
9 |
10 | func CfgInit() {
11 | var port string
12 | var addr string
13 |
14 | // Mongo ENV variables
15 | port = os.Getenv("MONGO_PORT_27017_TCP_PORT")
16 | addr = os.Getenv("MONGO_PORT_27017_TCP_ADDR")
17 | if port != "" && addr != "" {
18 | CfgMgoUrl = "mongodb://" + addr + ":" + port + "/puppy"
19 | }
20 |
21 | var kk = os.Getenv("TABLE")
22 | if kk != "" {
23 | CfgTable = kk
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/ngsw-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": "/index.html",
3 | "assetGroups": [{
4 | "name": "app",
5 | "installMode": "prefetch",
6 | "resources": {
7 | "files": [
8 | "/favicon.ico",
9 | "/index.html"
10 | ],
11 | "versionedFiles": [
12 | "/*.bundle.css",
13 | "/*.bundle.js",
14 | "/*.chunk.js"
15 | ]
16 | }
17 | }, {
18 | "name": "assets",
19 | "installMode": "lazy",
20 | "updateMode": "prefetch",
21 | "resources": {
22 | "files": [
23 | "/assets/**"
24 | ]
25 | }
26 | }]
27 | }
--------------------------------------------------------------------------------
/utils/http.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "io"
5 | "log"
6 | "net/http"
7 | "os"
8 |
9 | "github.com/pclubiitk/puppy-love/config"
10 | )
11 |
12 | func SignupRequest(id string) error {
13 | response, err := http.Get(config.SignupUrl + "/" + id)
14 | if err != nil {
15 | log.Println("Signup service is faulty")
16 | log.Println(err)
17 | return err
18 | } else {
19 | defer response.Body.Close()
20 | _, err := io.Copy(os.Stdout, response.Body)
21 | if err != nil {
22 | log.Println("Signup service is faulty")
23 | log.Println(err)
24 | }
25 | return err
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/app/components/student/student.component.css:
--------------------------------------------------------------------------------
1 | .img {
2 | background-size: cover;
3 | flex-grow: 1;
4 | min-width: 140px;
5 | width: 20%;
6 | }
7 |
8 | .info {
9 | flex-grow: 3;
10 | padding: 2vmin;
11 | width: 80%;
12 | }
13 |
14 | .name {
15 | font-weight: 700;
16 | }
17 |
18 | @media only screen and (max-width: 600px) {
19 | .container {
20 | width: 80vw !important;
21 | }
22 | }
23 |
24 | .container {
25 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
26 | cursor: pointer;
27 | height: 140px;
28 | margin: 2vmin;
29 | /* padding: 1vmin; */
30 | width: 400px;
31 | }
32 |
--------------------------------------------------------------------------------
/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 |
6 | "math/rand"
7 | "time"
8 | )
9 |
10 | func CheckForFields(c *gin.Context, required []string) bool {
11 | for _, key := range required {
12 | if c.Param(key) == "" {
13 | return false
14 | }
15 | }
16 | return true
17 | }
18 |
19 | func Randinit() {
20 | rand.Seed(time.Now().UnixNano())
21 | }
22 |
23 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
24 |
25 | func RandStringRunes(n int) string {
26 | b := make([]rune, n)
27 | for i := range b {
28 | b[i] = letterRunes[rand.Intn(len(letterRunes))]
29 | }
30 | return string(b)
31 | }
32 |
--------------------------------------------------------------------------------
/service/results/Gopkg.toml:
--------------------------------------------------------------------------------
1 |
2 | # Gopkg.toml example
3 | #
4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
5 | # for detailed Gopkg.toml documentation.
6 | #
7 | # required = ["github.com/user/thing/cmd/thing"]
8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
9 | #
10 | # [[constraint]]
11 | # name = "github.com/user/project"
12 | # version = "1.0.0"
13 | #
14 | # [[constraint]]
15 | # name = "github.com/user/project2"
16 | # branch = "dev"
17 | # source = "github.com/myfork/project2"
18 | #
19 | # [[override]]
20 | # name = "github.com/x/y"
21 | # version = "2.4.0"
22 |
23 |
24 | [[constraint]]
25 | name = "gopkg.in/mgo.v2"
26 |
--------------------------------------------------------------------------------
/service/signup/Gopkg.toml:
--------------------------------------------------------------------------------
1 |
2 | # Gopkg.toml example
3 | #
4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
5 | # for detailed Gopkg.toml documentation.
6 | #
7 | # required = ["github.com/user/thing/cmd/thing"]
8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
9 | #
10 | # [[constraint]]
11 | # name = "github.com/user/project"
12 | # version = "1.0.0"
13 | #
14 | # [[constraint]]
15 | # name = "github.com/user/project2"
16 | # branch = "dev"
17 | # source = "github.com/myfork/project2"
18 | #
19 | # [[override]]
20 | # name = "github.com/x/y"
21 | # version = "2.4.0"
22 |
23 |
24 | [[constraint]]
25 | name = "gopkg.in/mgo.v2"
26 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Gopkg.toml example
2 | #
3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
4 | # for detailed Gopkg.toml documentation.
5 | #
6 | # required = ["github.com/user/thing/cmd/thing"]
7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8 | #
9 | # [[constraint]]
10 | # name = "github.com/user/project"
11 | # version = "1.0.0"
12 | #
13 | # [[constraint]]
14 | # name = "github.com/user/project2"
15 | # branch = "dev"
16 | # source = "github.com/myfork/project2"
17 | #
18 | # [[override]]
19 | # name = "github.com/x/y"
20 | # version = "2.4.0"
21 |
22 |
23 | [[constraint]]
24 | name = "github.com/gin-gonic/gin"
25 | version = "1.2.0"
26 |
--------------------------------------------------------------------------------
/service/signup/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine as builder
2 |
3 | RUN apk --no-cache add openssl wget git
4 | ENV GOPATH=/go
5 | RUN go get -u github.com/golang/dep/cmd/dep
6 |
7 | # copies the Gopkg.toml and Gopkg.lock to WORKDIR
8 | RUN mkdir -p /go/src/github.com/pclubiitk/puppy-love/service/signup
9 | WORKDIR /go/src/github.com/pclubiitk/puppy-love/service/signup
10 | COPY Gopkg.toml Gopkg.lock ./
11 |
12 | RUN dep ensure -v -vendor-only
13 |
14 | COPY . .
15 | RUN go build
16 |
17 | FROM alpine
18 | RUN mkdir -p /go/bin
19 | COPY --from=builder /go/src/github.com/pclubiitk/puppy-love/service/signup/signup /go/bin
20 |
21 | RUN apk --no-cache add ca-certificates
22 | EXPOSE 3001
23 | ENTRYPOINT ["/go/bin/signup"]
24 |
--------------------------------------------------------------------------------
/service/results/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine as builder
2 |
3 | RUN apk --no-cache add openssl wget git
4 | ENV GOPATH=/go
5 | RUN go get -u github.com/golang/dep/cmd/dep
6 |
7 | # copies the Gopkg.toml and Gopkg.lock to WORKDIR
8 | RUN mkdir -p /go/src/github.com/pclubiitk/puppy-love/service/results
9 | WORKDIR /go/src/github.com/pclubiitk/puppy-love/service/results
10 | COPY Gopkg.toml Gopkg.lock ./
11 |
12 | RUN dep ensure -v -vendor-only
13 |
14 | COPY . .
15 | RUN go build
16 |
17 | FROM alpine
18 | RUN mkdir -p /go/bin
19 | COPY --from=builder /go/src/github.com/pclubiitk/puppy-love/service/results/results /go/bin
20 |
21 | RUN apk --no-cache add ca-certificates
22 | EXPOSE 3001
23 | ENTRYPOINT ["/go/bin/results"]
24 |
--------------------------------------------------------------------------------
/frontend/src/app/components/search/search.component.html:
--------------------------------------------------------------------------------
1 |
2 | search
3 |
4 |
5 |
6 |
7 | close
8 |
9 |
10 |
11 |
14 |
--------------------------------------------------------------------------------
/frontend/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /dist-server
6 | /tmp
7 | /out-tsc
8 |
9 | # dependencies
10 | /node_modules
11 |
12 | # IDEs and editors
13 | /.idea
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # IDE - VSCode
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 |
28 | # misc
29 | /.sass-cache
30 | /connect.lock
31 | /coverage
32 | /libpeerconnection.log
33 | npm-debug.log
34 | testem.log
35 | /typings
36 |
37 | # e2e
38 | /e2e/*.js
39 | /e2e/*.map
40 |
41 | # System Files
42 | .DS_Store
43 | Thumbs.db
44 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine as builder
2 |
3 | RUN apk --no-cache add openssl wget git
4 | ENV GOPATH=/go
5 | RUN wget -O /usr/local/bin/dep https://github.com/golang/dep/releases/download/v0.4.1/dep-$(go env GOOS)-$(go env GOHOSTARCH) && chmod +x /usr/local/bin/dep
6 |
7 | RUN mkdir -p /go/src/github.com/pclubiitk/puppy-love
8 | WORKDIR /go/src/github.com/pclubiitk/puppy-love
9 |
10 | COPY Gopkg.toml Gopkg.lock ./
11 | # copies the Gopkg.toml and Gopkg.lock to WORKDIR
12 |
13 | RUN dep ensure -v -vendor-only
14 |
15 | COPY . .
16 | RUN go build
17 |
18 | FROM alpine
19 | RUN mkdir -p /go/bin
20 | COPY --from=builder /go/src/github.com/pclubiitk/puppy-love/puppy-love /go/bin
21 |
22 | EXPOSE 3000
23 | ENTRYPOINT ["/go/bin/puppy-love"]
24 |
25 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/index.ts:
--------------------------------------------------------------------------------
1 | import { AboutComponent } from './about';
2 | import { HowItWorksComponent } from './how-it-works';
3 | import { HomeComponent } from './home';
4 | import { HowToComponent } from './howto';
5 | import { LoginComponent } from './login';
6 | import { MainComponent } from './main';
7 | import { SignupComponent } from './signup';
8 |
9 | export const containers = [
10 | AboutComponent,
11 | HomeComponent,
12 | HowItWorksComponent,
13 | HowToComponent,
14 | LoginComponent,
15 | MainComponent,
16 | SignupComponent,
17 | ];
18 |
19 | export * from './about';
20 | export * from './home';
21 | export * from './how-it-works';
22 | export * from './howto';
23 | export * from './login';
24 | export * from './main';
25 | export * from './signup';
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/home/home.component.scss:
--------------------------------------------------------------------------------
1 | [mat-button] {
2 | width: 100%;
3 | }
4 |
5 | mat-form-field {
6 | width: 100%;
7 | }
8 |
9 | mat-card-title {
10 | text-align: center;
11 | }
12 |
13 | .profile {
14 | width: 20vw;
15 | }
16 |
17 | .profile-pic {
18 | width: 20vw;
19 | height: 20vw;
20 | background-size: cover;
21 | padding: 24px;
22 | flex-grow: 1;
23 | }
24 |
25 | .main {
26 | width: 66.67vw;
27 | }
28 |
29 | .smallonly {
30 | display: none;
31 | }
32 |
33 | .lovely-heart {
34 | max-width: 50px;
35 | }
36 |
37 | @media screen and (max-width: 640px) {
38 | .smallonly {
39 | display: block;
40 | }
41 | .main {
42 | display: block;
43 | width: 90vw;
44 | }
45 | .bigonly {
46 | display: none;
47 | }
48 | .textonly-greeter {
49 | margin: 5px;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/frontend/src/app/components/login/login.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, Output } from '@angular/core';
2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms';
3 |
4 | @Component({
5 | selector: 'puppy-login',
6 | templateUrl: './login.component.html',
7 | styleUrls: [ './login.component.scss' ]
8 | })
9 | export class LoginComponent {
10 |
11 | loginForm: FormGroup;
12 |
13 | @Output()
14 | private login = new EventEmitter<{roll: string, password: string}>();
15 |
16 | constructor(private fb: FormBuilder) {
17 | // Create Form
18 | this.loginForm = this.fb.group({
19 | roll: ['', Validators.required],
20 | password: ['', Validators.required],
21 | });
22 | }
23 |
24 | get loginInfo(): {roll: string, password: string} {
25 | return this.loginForm.value;
26 | }
27 |
28 | onSubmit() {
29 | this.login.emit(this.loginInfo);
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/app/components/index.ts:
--------------------------------------------------------------------------------
1 | import { AboutComponent } from './about';
2 | import { CountdownComponent } from './countdown';
3 | import { HomeComponent } from './home';
4 | import { HowToComponent } from './howto';
5 | import { LoginComponent } from './login';
6 | import { ResultsComponent } from './results';
7 | import { SearchComponent } from './search';
8 | import { SignupComponent } from './signup';
9 | import { StudentComponent } from './student';
10 |
11 | export const components = [
12 | AboutComponent,
13 | CountdownComponent,
14 | HomeComponent,
15 | HowToComponent,
16 | LoginComponent,
17 | ResultsComponent,
18 | SearchComponent,
19 | SignupComponent,
20 | StudentComponent,
21 | ];
22 |
23 | export * from './about';
24 | export * from './countdown';
25 | export * from './home';
26 | export * from './howto';
27 | export * from './login';
28 | export * from './results';
29 | export * from './search';
30 | export * from './signup';
31 | export * from './student';
32 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | mongo:
5 | image: mongo
6 | networks:
7 | - mynet
8 |
9 | results:
10 | image: pclub/puppy-love:results
11 | env_file:
12 | - .backend.env
13 | links:
14 | - mongo
15 | networks:
16 | - mynet
17 |
18 | signup:
19 | image: pclub/puppy-love:signup
20 | env_file:
21 | - .backend.env
22 | - .mail.env
23 | ports:
24 | - "3001"
25 | links:
26 | - mongo
27 | networks:
28 | - mynet
29 |
30 | backend:
31 | image: pclub/puppy-love:backend
32 | env_file:
33 | - .backend.env
34 | - .pass.env
35 | ports:
36 | - "34770:3000"
37 | links:
38 | - mongo
39 | - signup
40 | networks:
41 | - mynet
42 |
43 | frontend:
44 | image: pclub/puppy-love:frontend
45 | ports:
46 | - "34771:80"
47 |
48 | networks:
49 | mynet:
50 |
--------------------------------------------------------------------------------
/frontend/src/app/components/login/login.component.html:
--------------------------------------------------------------------------------
1 |
2 | Login
3 |
4 |
5 |
26 |
27 |
28 |
29 | Note that you need to create an account before you can log in.
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/src/app/components/student/student.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, Input, Output } from '@angular/core';
2 | import { DomSanitizer } from '@angular/platform-browser';
3 |
4 | function ImageURL(rollnum: string, userid: string) {
5 | const iitkhome = `http://home.iitk.ac.in/~${ userid }/dp`;
6 | const oaimage = `https://oa.cc.iitk.ac.in/Oa/Jsp/Photo/${ rollnum }_0.jpg`;
7 | return `url("${ iitkhome }"), url("${ oaimage }")`;
8 | }
9 |
10 | @Component({
11 | selector: 'puppy-student',
12 | templateUrl: './student.component.html',
13 | styleUrls: ['./student.component.css'],
14 | })
15 | export class StudentComponent {
16 |
17 | @Input()
18 | student: any;
19 | @Output()
20 | select = new EventEmitter();
21 |
22 | constructor(private sanitizer: DomSanitizer) {}
23 |
24 | get url() {
25 | return this.sanitizer.bypassSecurityTrustStyle(ImageURL(this.student._id, this.student.email));
26 | }
27 |
28 | selectStudent() {
29 | this.select.emit();
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/login/login.container.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import { MatSnackBar } from '@angular/material/snack-bar';
4 | import 'rxjs/add/operator/finally';
5 |
6 | import { MainService } from '../../services/main.service';
7 |
8 | @Component({
9 | templateUrl: './login.container.html',
10 | styleUrls: [ './login.container.scss' ]
11 | })
12 | export class LoginComponent {
13 |
14 | loading = false;
15 |
16 | constructor(private main: MainService,
17 | private router: Router,
18 | private snackBar: MatSnackBar) {}
19 |
20 | onLogin(login: { roll: string, password: string }) {
21 | this.loading = true;
22 | this.main.login(login.roll, login.password)
23 | .finally(() => this.loading = false)
24 | .subscribe(
25 | () => this.router.navigate([ 'home' ]),
26 | (err) => this.snackBar.open(err, '', {
27 | duration: 3000
28 | }));
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/pclubiitk/puppy-love/config"
8 | "github.com/pclubiitk/puppy-love/db"
9 | "github.com/pclubiitk/puppy-love/router"
10 | "github.com/pclubiitk/puppy-love/utils"
11 |
12 | "github.com/gin-contrib/sessions"
13 | "github.com/gin-gonic/gin"
14 | )
15 |
16 | func executeFirst(c *gin.Context) {
17 | // fmt.Println(string(ctx.Path()[:]))
18 | // ctx.Next()
19 | }
20 |
21 | func main() {
22 | config.CfgInit()
23 |
24 | mongoDb, error := db.MongoConnect()
25 | if error != nil {
26 | fmt.Print("[Error] Could not connect to MongoDB")
27 | fmt.Print("[Error] " + config.CfgMgoUrl)
28 | fmt.Print(os.Environ())
29 | os.Exit(1)
30 | }
31 |
32 | utils.Randinit()
33 |
34 | // set up session db
35 | store := sessions.NewCookieStore([]byte(config.CfgAdminPass))
36 |
37 | // iris.Config.Gzip = true
38 | r := gin.Default()
39 | r.Use(sessions.Sessions("mysession", store))
40 | router.PuppyRoute(r, mongoDb)
41 | r.Run(config.CfgAddr)
42 | }
43 |
--------------------------------------------------------------------------------
/service/signup/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 | )
7 |
8 | var EmailUser = os.Getenv("EMAIL_USER")
9 | var EmailPass = os.Getenv("EMAIL_PASS")
10 | var EmailHost = os.Getenv("EMAIL_HOST")
11 | var EmailPort = os.Getenv("EMAIL_PORT")
12 |
13 | var CfgAddr = ":3000"
14 |
15 | var CfgMgoUrl = "mongodb://0.0.0.0:27017/puppy"
16 |
17 | func CfgInit() {
18 | var port string
19 | var addr string
20 |
21 | // Email ENV variables
22 | if EmailUser == "" || EmailPass == "" {
23 | log.Println("WARNING: Email variables are not in scope")
24 | }
25 |
26 | if EmailHost == "" {
27 | log.Println("No email host")
28 | EmailHost = "smtp.gmail.com"
29 | }
30 |
31 | if EmailPort == "" {
32 | log.Println("No email port")
33 | EmailHost = "587"
34 | }
35 |
36 | // Mongo ENV variables
37 | port = os.Getenv("MONGO_PORT_27017_TCP_PORT")
38 | addr = os.Getenv("MONGO_PORT_27017_TCP_ADDR")
39 | if port != "" && addr != "" {
40 | CfgMgoUrl = "mongodb://" + addr + ":" + port + "/puppy"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/scripts/script.js:
--------------------------------------------------------------------------------
1 | const MongoClient = require('mongodb').MongoClient;
2 | const url = 'mongodb://localhost:27017/';
3 | const generatePassword = require('password-generator');
4 |
5 | const students = require('./students.json');
6 |
7 | MongoClient.connect(url, function(err, db) {
8 | if (err) throw err;
9 | console.log('Database created!');
10 | const dbo = db.db('puppy');
11 | const results = students.map((s) => {
12 | return {
13 | _id: s.i,
14 | name: s.n,
15 | email: s.u,
16 | image: '',
17 | gender: s.g === 'M' ? '1' : '0',
18 | passHash: 'aaaa',
19 | privKey: '',
20 | pubKey: '',
21 | authCode: generatePassword(15),
22 | data: '',
23 | submitted: false,
24 | matches: '',
25 | voted: 0,
26 | dirty: true,
27 | savepass: ''
28 | };
29 | });
30 | dbo.collection('user').insertMany(results , function(err, res) {
31 | if (err) throw err;
32 | console.log("Number of documents inserted: " + res.insertedCount);
33 | db.close();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Programming Club IIT Kanpur
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/frontend/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular/cli'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular/cli/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | reports: [ 'html', 'lcovonly' ],
20 | fixWebpackSourcePaths: true
21 | },
22 | angularCli: {
23 | environment: 'dev'
24 | },
25 | reporters: ['progress', 'kjhtml'],
26 | port: 9876,
27 | colors: true,
28 | logLevel: config.LOG_INFO,
29 | autoWatch: true,
30 | browsers: ['Chrome'],
31 | singleRun: false
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/models/declare.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "gopkg.in/mgo.v2"
5 | "gopkg.in/mgo.v2/bson"
6 | )
7 |
8 | type (
9 | Declare struct {
10 | Id string `json:"_id" bson:"_id"`
11 | Token0 string `json:"t0" bson:"t0"`
12 | Token1 string `json:"t1" bson:"t1"`
13 | Token2 string `json:"t2" bson:"t2"`
14 | Token3 string `json:"t3" bson:"t3"`
15 | }
16 | PairUpsert struct {
17 | Selector bson.M
18 | Change bson.M
19 | }
20 | )
21 |
22 | // Create table update object for token table
23 | func UpsertDeclareTable(d *Declare) mgo.Change {
24 | return mgo.Change{
25 | Update: bson.M{"$set": bson.M{
26 | "t0": d.Token0,
27 | "t1": d.Token1,
28 | "t2": d.Token2,
29 | "t3": d.Token3,
30 | }},
31 | ReturnNew: true,
32 | }
33 | }
34 |
35 | func NewDeclareTable(id string) PairUpsert {
36 | _selector := bson.M{"_id": id}
37 |
38 | return PairUpsert{
39 | Selector: _selector,
40 | Change: bson.M{"$setOnInsert": bson.M{
41 | "_id": id,
42 | "t0": "",
43 | "t1": "",
44 | "t2": "",
45 | "t3": "",
46 | }},
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Frontend
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.6.5.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
28 |
--------------------------------------------------------------------------------
/frontend/src/app/components/results/results.component.scss:
--------------------------------------------------------------------------------
1 | [mat-button] {
2 | width: 100%;
3 | }
4 |
5 | mat-form-field {
6 | width: 100%;
7 | }
8 |
9 | mat-card-title {
10 | text-align: center;
11 | }
12 |
13 | .profile {
14 | width: 20vw;
15 | }
16 |
17 | .profile-pic {
18 | width: 20vw;
19 | height: 20vw;
20 | background-size: cover;
21 | padding: 24px;
22 | flex-grow: 1;
23 | }
24 |
25 | .main {
26 | margin-bottom: 30px;
27 | width: 66.67vw;
28 | }
29 |
30 | .smallonly {
31 | display: none;
32 | }
33 |
34 | .lovely-heart {
35 | max-width: 50px;
36 | }
37 |
38 | .chart {
39 | border: 1px solid black;
40 | width: 18vw;
41 | height: 18vw;
42 | padding: 10px;
43 | margin: 10px;
44 |
45 | h2 {
46 | margin: 0;
47 | text-align: center;
48 | }
49 | }
50 |
51 | @media screen and (max-width: 640px) {
52 | .smallonly {
53 | display: block;
54 | }
55 | .main {
56 | display: block;
57 | width: 80vw;
58 | }
59 | .bigonly {
60 | display: none;
61 | }
62 | .textonly-greeter {
63 | margin: 5px;
64 | }
65 | .chart {
66 | width: 63vw;
67 | height: 63vw;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/puppy.nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name dev.puppy.pclub.in;
4 |
5 | location /api {
6 | # proxy_pass http://pclub.cse.iitk.ac.in;
7 | proxy_pass http://localhost:3000;
8 | proxy_set_header Connection '';
9 | proxy_set_header Host 'pclub.cse.iitk.ac.in';
10 | proxy_set_header Referrer 'pclub.cse.iitk.ac.in';
11 | add_header 'Access-Control-Allow-Origin' $host;
12 | proxy_http_version 1.1;
13 | chunked_transfer_encoding off;
14 | proxy_connect_timeout 5m;
15 | proxy_read_timeout 5m;
16 | }
17 |
18 | location /Oa {
19 | proxy_pass http://oa.cc.iitk.ac.in:80;
20 | proxy_set_header Connection '';
21 | add_header 'Access-Control-Allow-Origin' $host;
22 | proxy_http_version 1.1;
23 | chunked_transfer_encoding off;
24 | proxy_connect_timeout 5m;
25 | proxy_read_timeout 5m;
26 | }
27 |
28 | location / {
29 | proxy_pass http://localhost:8091/;
30 | proxy_http_version 1.1;
31 | proxy_read_timeout 86400;
32 | proxy_set_header Host 'pclub.cse.iitk.ac.in';
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/service/results/user.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "gopkg.in/mgo.v2"
4 | import "gopkg.in/mgo.v2/bson"
5 |
6 | type (
7 | // User represents the structure of our resource
8 | User struct {
9 | Id string `json:"_id" bson:"_id"`
10 | Name string `json:"name" bson:"name"`
11 | Email string `json:"email" bson:"email"`
12 | Gender string `json:"gender" bson:"gender"`
13 | Image string `json:"image" bson:"image"`
14 | Pass string `json:"passHash" bson:"passHash"`
15 | PrivK string `json:"privKey" bson:"privKey"`
16 | PubK string `json:"pubKey" bson:"pubKey"`
17 | AuthC string `json:"authCode" bson:"authCode"`
18 | Data string `json:"data" bson:"data"`
19 | Submit bool `json:"submitted" bson:"submitted"`
20 | Matches string `json:"matches" bson:"matches"`
21 | Vote int `json:"voted" bson:"voted"`
22 | Dirty bool `json:"dirty" bson:"dirty"`
23 | SPass string `json:"savepass" bson:"savepass"`
24 | }
25 | )
26 |
27 | func (u User) MatchedUpdate(info string) mgo.Change {
28 | return mgo.Change{
29 | Update: bson.M{"$set": bson.M{
30 | "matches": info,
31 | }},
32 | ReturnNew: true,
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/service/nginx/nginx.prod.conf:
--------------------------------------------------------------------------------
1 | worker_processes 8;
2 |
3 | events { worker_connections 4096; }
4 |
5 | http {
6 |
7 | server {
8 | include /etc/nginx/mime.types;
9 |
10 | listen 80;
11 |
12 | location /api {
13 | proxy_pass http://backend:3000;
14 | rewrite /api/(.*) /$1 break;
15 | proxy_set_header Connection '';
16 | add_header 'Access-Control-Allow-Origin' $host;
17 | proxy_http_version 1.1;
18 | chunked_transfer_encoding off;
19 | proxy_connect_timeout 5m;
20 | proxy_read_timeout 5m;
21 | }
22 |
23 | location ~* ^/Oa/Jsp/Photo/.+\.(jpg)$ {
24 | proxy_pass http://oa.cc.iitk.ac.in:80;
25 | proxy_set_header Connection '';
26 | add_header 'Access-Control-Allow-Origin' $host;
27 | proxy_http_version 1.1;
28 | chunked_transfer_encoding off;
29 | proxy_connect_timeout 5m;
30 | proxy_read_timeout 5m;
31 | }
32 |
33 | location / {
34 | proxy_pass http://frontend;
35 | proxy_set_header Connection '';
36 | add_header 'Access-Control-Allow-Origin' $host;
37 | proxy_http_version 1.1;
38 | chunked_transfer_encoding off;
39 | proxy_connect_timeout 5m;
40 | proxy_read_timeout 5m;
41 | }
42 |
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/controllers/declare.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "log"
5 | "net/http"
6 |
7 | "github.com/pclubiitk/puppy-love/models"
8 |
9 | "github.com/gin-gonic/gin"
10 | "gopkg.in/mgo.v2/bson"
11 | )
12 |
13 | // @AUTH @Admin Create the entries in the declare table
14 | // ----------------------------------------------------
15 | func DeclarePrepare(c *gin.Context) {
16 | id, err := SessionId(c)
17 | if err != nil || id != "admin" {
18 | c.AbortWithStatus(http.StatusForbidden)
19 | return
20 | }
21 |
22 | type typeIds struct {
23 | Id string `json:"_id" bson:"_id"`
24 | }
25 |
26 | var people []typeIds
27 |
28 | if err := Db.GetCollection("user").
29 | Find(bson.M{"dirty": false}).
30 | All(&people); err != nil {
31 | c.AbortWithStatus(http.StatusInternalServerError)
32 | return
33 | }
34 |
35 | bulk := Db.GetCollection("declare").Bulk()
36 | for _, pe := range people {
37 | res := models.NewDeclareTable(pe.Id)
38 | bulk.Upsert(res.Selector, res.Change)
39 | }
40 | r, err := bulk.Run()
41 |
42 | if err != nil {
43 | c.AbortWithStatus(http.StatusInternalServerError)
44 | log.Println(err)
45 | return
46 | }
47 | c.JSON(http.StatusOK, r)
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 |
4 | import { LoggedInGuard } from './guards/logged-in.guard';
5 | import { LoggedOutGuard } from './guards/logged-out.guard';
6 |
7 | import {
8 | AboutComponent,
9 | HowItWorksComponent,
10 | HowToComponent,
11 | LoginComponent,
12 | MainComponent,
13 | SignupComponent,
14 | HomeComponent,
15 | } from './containers';
16 |
17 | const routes: Routes = [{
18 | path: '', component: MainComponent, children: [
19 | { path: '', pathMatch: 'full', redirectTo: 'login' },
20 | { path: 'about', component: AboutComponent },
21 | { path: 'how-it-works', component: HowItWorksComponent },
22 | { path: 'howto', component: HowToComponent },
23 | { path: 'login', component: LoginComponent, canActivate: [ LoggedOutGuard ] },
24 | { path: 'signup', component: SignupComponent, canActivate: [ LoggedOutGuard ] },
25 | ]
26 | }, { path: 'home', component: HomeComponent, canActivate: [ LoggedInGuard ] }
27 | ];
28 |
29 | @NgModule({
30 | imports: [RouterModule.forRoot(routes)],
31 | exports: [RouterModule]
32 | })
33 | export class AppRoutingModule { }
34 |
--------------------------------------------------------------------------------
/frontend/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, async } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './app.component';
4 | describe('AppComponent', () => {
5 | beforeEach(async(() => {
6 | TestBed.configureTestingModule({
7 | imports: [
8 | RouterTestingModule
9 | ],
10 | declarations: [
11 | AppComponent
12 | ],
13 | }).compileComponents();
14 | }));
15 | it('should create the app', async(() => {
16 | const fixture = TestBed.createComponent(AppComponent);
17 | const app = fixture.debugElement.componentInstance;
18 | expect(app).toBeTruthy();
19 | }));
20 | it(`should have as title 'puppy'`, async(() => {
21 | const fixture = TestBed.createComponent(AppComponent);
22 | const app = fixture.debugElement.componentInstance;
23 | expect(app.title).toEqual('puppy');
24 | }));
25 | it('should render title in a h1 tag', async(() => {
26 | const fixture = TestBed.createComponent(AppComponent);
27 | fixture.detectChanges();
28 | const compiled = fixture.debugElement.nativeElement;
29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to puppy!');
30 | }));
31 | });
32 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "os"
5 | )
6 |
7 | var (
8 | EmailUser = os.Getenv("EMAIL_USER")
9 | EmailPass = os.Getenv("EMAIL_PASS")
10 | CfgAdminPass = "passhash"
11 | CfgAddr = ":3000"
12 | CfgMgoUrl = "mongodb://0.0.0.0:27017/puppy"
13 | CfgRedisUrl = "0.0.0.0:6379"
14 | SignupUrl string = "http://0.0.0.0:3001"
15 | )
16 |
17 | func CfgInit() {
18 | var port string
19 | var addr string
20 |
21 | // Signup URL
22 | port = os.Getenv("SIGNUP_PORT_3001_TCP_PORT")
23 | addr = os.Getenv("SIGNUP_PORT_3001_TCP_ADDR")
24 | if port != "" && addr != "" {
25 | SignupUrl = "http://" + addr + ":" + port
26 | }
27 |
28 | // Mongo ENV variables
29 | port = os.Getenv("MONGO_PORT_27017_TCP_PORT")
30 | addr = os.Getenv("MONGO_PORT_27017_TCP_ADDR")
31 | if port != "" && addr != "" {
32 | CfgMgoUrl = "mongodb://" + addr + ":" + port + "/puppy"
33 | }
34 |
35 | // Redis ENV variables
36 | port = os.Getenv("REDIS_PORT_6379_TCP_PORT")
37 | addr = os.Getenv("REDIS_PORT_6379_TCP_ADDR")
38 | if port != "" && addr != "" {
39 | CfgRedisUrl = addr + ":" + port
40 | }
41 |
42 | // Admin pass
43 | pass := os.Getenv("ADMIN_PASS")
44 | if pass != "" {
45 | CfgAdminPass = pass
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/src/app/components/about/about.component.html:
--------------------------------------------------------------------------------
1 |
2 | About
3 |
4 |
5 |
6 | Who said win-win situations do not exist?
7 |
8 | Puppy Love helps you meet your crush, anonymously.
9 |
10 |
11 | Express your interest, and both of you will be informed if the
12 | interest is mutual. Otherwise, no one will EVER
13 | know about your choice. No, not even the admin , i.e. us.
14 |
15 | Features
16 |
17 | If you do not match with someone, no one can ever know your choice, not even the administrator
18 | No information will ever leave your browser which lets the administrator or anyone else see your choices
19 | Your login password (of puppy love) will never be sent to the server, even for login. We use secure hashes.
20 | Algorithm reviewed by professors and peers in terms of security and confidentiality
21 |
22 | Want to read the algorithm?
23 | Puppy Love Alogorithm Report
24 |
25 |
26 | Back
27 |
28 |
--------------------------------------------------------------------------------
/frontend/src/app/containers/signup/signup.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import { MatSnackBar } from '@angular/material/snack-bar';
4 | import 'rxjs/add/operator/finally';
5 |
6 | import { MainService } from '../../services/main.service';
7 |
8 | @Component({
9 | templateUrl: './signup.component.html',
10 | styleUrls: [ './signup.component.scss' ]
11 | })
12 | export class SignupComponent {
13 |
14 | loading = false;
15 |
16 | constructor(private main: MainService,
17 | private router: Router,
18 | private snackBar: MatSnackBar) {}
19 |
20 | signup(event: any) {
21 | this.loading = true;
22 | this.main.signup(event)
23 | .finally(() => this.loading = false)
24 | .subscribe(
25 | () => this.router.navigate([ 'login' ]),
26 | (err) => this.snackBar.open(err, '', {
27 | duration: 3000
28 | }));
29 | }
30 |
31 | mail(roll: string) {
32 | this.loading = true;
33 | this.main.mail(roll)
34 | .finally(() => this.loading = false)
35 | .subscribe(
36 | (msg) => {
37 | console.log(msg);
38 | this.snackBar.open(msg, '', {
39 | duration: 3000
40 | })
41 | });
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/controllers/heart.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "strconv"
7 | "time"
8 |
9 | "github.com/gin-gonic/gin"
10 |
11 | "gopkg.in/mgo.v2/bson"
12 | )
13 |
14 | func HeartGet(c *gin.Context) {
15 | id, err := SessionId(c)
16 | if err != nil || id != c.Param("you") {
17 | c.AbortWithStatus(http.StatusForbidden)
18 | return
19 | }
20 |
21 | // Last checked time
22 | ltime, err := strconv.ParseUint(c.Param("time"), 10, 64)
23 | if err != nil {
24 | c.String(http.StatusBadRequest, "Bad timestamp value")
25 | return
26 | }
27 |
28 | // Current time
29 | ctime := uint64(time.Now().UnixNano() / 1000000)
30 |
31 | // TODO: fix bindings to be consistent across dbs
32 | type AnonymVote struct {
33 | Value string `json:"v" bson:"v"`
34 | GenderOfSender string `json:"genderOfSender" bson:"gender"`
35 | }
36 |
37 | votes := new([]AnonymVote)
38 |
39 | // Fetch user
40 | if err := Db.GetCollection("heart").
41 | Find(bson.M{"time": bson.M{"$gt": ltime, "$lte": ctime}}).
42 | All(votes); err != nil {
43 | c.AbortWithStatus(http.StatusNotFound)
44 | log.Print(err)
45 | return
46 | }
47 |
48 | if *votes == nil {
49 | *votes = []AnonymVote{}
50 | }
51 |
52 | c.JSON(http.StatusAccepted, bson.M{
53 | "votes": *votes,
54 | "time": ctime,
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/service/signup/signup.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "os"
8 | "os/signal"
9 | )
10 |
11 | type appHandler struct {
12 | Channel chan string
13 | }
14 |
15 | func (ctx appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
16 | ctx.Channel <- r.URL.Path[:]
17 | fmt.Fprintf(w, "Welcome, %s!", r.URL.Path[1:])
18 | }
19 |
20 | func main() {
21 | CfgInit()
22 | mongoDb, error := MongoConnect()
23 | if error != nil {
24 | fmt.Print("[Error] Could not connect to MongoDB")
25 | os.Exit(1)
26 | }
27 |
28 | // Create channels for services
29 | signup_channel := make(chan string)
30 | mailer_channel := make(chan User)
31 | queue_channel := make(chan string)
32 | go SignupService(mongoDb, signup_channel, mailer_channel)
33 | go MailerService(mongoDb, mailer_channel)
34 | go QueueService(queue_channel, signup_channel)
35 |
36 | // For clean exit
37 | c := make(chan os.Signal, 1)
38 | signal.Notify(c, os.Interrupt)
39 | go func() {
40 | for sig := range c {
41 | log.Printf("Captured %v, exiting..", sig)
42 | close(signup_channel)
43 | close(mailer_channel)
44 | close(queue_channel)
45 | // TODO exit mongo and redis too
46 | os.Exit(0)
47 | }
48 | }()
49 |
50 | http.Handle("/", appHandler{queue_channel})
51 | http.ListenAndServe(":3001", nil)
52 | }
53 |
--------------------------------------------------------------------------------
/frontend/src/app/components/search/search.component.css:
--------------------------------------------------------------------------------
1 | .width-60 {
2 | background-color: #fff;
3 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
4 | margin: 0 auto;
5 | padding-top: 20px;
6 | width: 60%;
7 | }
8 |
9 | @media (max-width: 600px) {
10 | .width-60 {
11 | width: 90%;
12 | }
13 | }
14 |
15 | .search-options {
16 | margin: 15px;
17 | padding: 5px;
18 | }
19 |
20 | .mat-select-underline {
21 | background-color: #fff !important;
22 | color: #fff;
23 | }
24 |
25 | .text-area {
26 | background-color: #fff;
27 | }
28 |
29 | .search-grid {
30 | background-color: #eee;
31 | width: 100%;
32 | }
33 |
34 | .search-grid button {
35 | margin: 10px;
36 | }
37 |
38 | .search-icon {
39 | margin: 18px;
40 | }
41 |
42 | mat-spinner {
43 | margin: 10vh auto;
44 | }
45 |
46 | masonry {
47 | margin: 0 auto;
48 | width: 80%;
49 | }
50 |
51 | .fab {
52 | bottom: 2%;
53 | height: 2.8rem;
54 | position: fixed;
55 | right: 2%;
56 | text-align: center;
57 | width : 2.8rem;
58 | }
59 |
60 | .fab mat-icon {
61 | height: 2.8rem;
62 | width : 2.8rem;
63 | }
64 |
65 | .help {
66 | height: 2.8rem;
67 | position: fixed;
68 | right: 2%;
69 | top: 2%;
70 | width : 2.8rem;
71 | }
72 |
73 | .result-count {
74 | background-color: #eeeeee;
75 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
76 | margin: 10px;
77 | padding: 10px;
78 | }
79 |
--------------------------------------------------------------------------------
/frontend/src/app/components/signup/signup.component.html:
--------------------------------------------------------------------------------
1 |
2 | Send verification mail
3 |
4 |
5 |
15 |
16 |
17 |
18 | Signup
19 |
20 |
21 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/frontend/src/app/components/countdown/countdown.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | import * as moment from 'moment';
4 | import { interval } from 'rxjs/observable/interval';
5 | import { Subscription } from 'rxjs/Subscription';
6 |
7 | @Component({
8 | selector: 'puppy-countdown',
9 | templateUrl: './countdown.component.html',
10 | styleUrls: [ './countdown.component.scss' ]
11 | })
12 | export class CountdownComponent implements OnInit {
13 | current: string;
14 | subs: Subscription;
15 |
16 | ngOnInit() {
17 | this.updateCurrent();
18 | this.subs = interval(1000).subscribe(() => this.updateCurrent());
19 | }
20 |
21 | ngOnDestroy() {
22 | if (this.subs)
23 | this.subs.unsubscribe();
24 | }
25 |
26 | updateCurrent() {
27 | const eventdate = moment([2019, 1, 8]);
28 | const todaysdate = moment();
29 | const totalSeconds = eventdate.diff(todaysdate, 'seconds');
30 | const seconds = totalSeconds % 60;
31 | const totalminutes = Math.floor(totalSeconds / 60);
32 | const minutes = totalminutes % 60;
33 | const hours = Math.floor(totalminutes / 60);
34 | let hourString = "" + hours;
35 | let minuteString = "" + minutes;
36 | let secondsString = "" + seconds;
37 | if (hourString.length < 2) {
38 | hourString = "0" + hourString;
39 | }
40 | if (minuteString.length < 2) {
41 | minuteString = "0" + minuteString;
42 | }
43 | if (secondsString.length < 2) {
44 | secondsString = "0" + secondsString;
45 | }
46 | this.current = `${ hourString }:${ minuteString }:${ secondsString }`;
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/frontend/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "frontend"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets",
12 | "favicon.ico"
13 | ],
14 | "index": "index.html",
15 | "main": "main.ts",
16 | "polyfills": "polyfills.ts",
17 | "test": "test.ts",
18 | "tsconfig": "tsconfig.app.json",
19 | "testTsconfig": "tsconfig.spec.json",
20 | "prefix": "puppy",
21 | "styles": [
22 | "styles.scss",
23 | "../node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
24 | ],
25 | "scripts": [
26 | "sjcl.js"
27 | ],
28 | "environmentSource": "environments/environment.ts",
29 | "environments": {
30 | "dev": "environments/environment.ts",
31 | "prod": "environments/environment.prod.ts"
32 | },
33 | "serviceWorker": true
34 | }
35 | ],
36 | "e2e": {
37 | "protractor": {
38 | "config": "./protractor.conf.js"
39 | }
40 | },
41 | "lint": [
42 | {
43 | "project": "src/tsconfig.app.json",
44 | "exclude": "**/node_modules/**"
45 | },
46 | {
47 | "project": "src/tsconfig.spec.json",
48 | "exclude": "**/node_modules/**"
49 | },
50 | {
51 | "project": "e2e/tsconfig.e2e.json",
52 | "exclude": "**/node_modules/**"
53 | }
54 | ],
55 | "test": {
56 | "karma": {
57 | "config": "./karma.conf.js"
58 | }
59 | },
60 | "defaults": {
61 | "styleExt": "scss",
62 | "component": {}
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/scripts/allusers.py:
--------------------------------------------------------------------------------
1 | import requests as r
2 | import sys
3 |
4 | with open(sys.argv[1]) as f:
5 | text = f.readlines()
6 |
7 | if len(sys.argv) >= 3:
8 | host = sys.argv[2]
9 | else:
10 | host = 'http://localhost:3000'
11 |
12 | url = host + '/admin/user/new'
13 | loginurl = host + '/session/login'
14 | logouturl = host + '/session/logout'
15 |
16 | ROLLNUM=0
17 | NAME=2
18 | EMAIL=7
19 | GENDER=9
20 | IMAGE=10
21 |
22 | s = r.Session()
23 | print("Logging in")
24 | logininfo = {}
25 | logininfo["username"] = "admin"
26 | logininfo["password"] = "passhash"
27 | resp = s.post(loginurl, json=logininfo)
28 | print(resp.status_code)
29 | print("Cookie: ", end='')
30 | print(s.cookies)
31 |
32 | if resp.status_code != 200:
33 | print("Could not login")
34 | exit()
35 |
36 | for person in text:
37 | data = list(map(lambda x: x.strip(), person.split(',')))
38 |
39 | payload = {}
40 |
41 | payload['roll'] = data[ROLLNUM]
42 | payload['name'] = data[NAME][1:-1]
43 | payload['email'] = data[EMAIL].split('@')[0]
44 | payload['image'] = data[IMAGE]
45 | if payload['email'] == "\"Not Available\"":
46 | continue
47 |
48 | if data[GENDER] == 'M':
49 | payload['gender'] = "1"
50 | elif data[GENDER] == 'F':
51 | payload['gender'] = "0"
52 | else:
53 | print("ERROR: finding gender of person {}".format(payload[roll]))
54 | continue
55 |
56 | print(payload)
57 |
58 | resp = s.post(url, json=payload)
59 | print("Code: " + str(resp.status_code))
60 | if resp.status_code > 299:
61 | print(resp.text)
62 |
63 | print("Logging out... ", end='')
64 | print(s.get(logouturl).status_code)
65 |
--------------------------------------------------------------------------------
/frontend/src/app/components/howto/howto.component.html:
--------------------------------------------------------------------------------
1 |
2 | How To Use
3 |
4 |
5 |
6 | Puppy Love lets you find mutual interests among people on campus. And it is simple to use!
7 |
8 |
9 | The platform is ours, but the love-algorithm is yours! Express your puppy love, and enjoy!
10 |
11 | Steps
12 |
13 |
14 | Create a new account, through an email confirmation, and then setting up a password.
15 | Make sure you do not forget the password. If lost, your data cannot be recovered.
16 | We do not send your plain password to the server even for login, so be assured :)
17 |
18 | Log in to your account.
19 | Select up to 4 of your interests.
20 | These choices will NOT be visible to anyone else.
21 | Lock and submit your choices! Remember, you cannot change your choices after this.
22 | Check back regularly to receive hearts signifying how many people like you :)
23 | Open the platform at midnight on 14th of February to know if one or more of your interests likes you back :)
24 | Remember, if your interest does not like you back, he/she will NEVER know that you like them.
25 |
26 | Good Luck!
27 |
28 |
29 | Back
30 |
31 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "ng serve --proxy proxy.conf.json",
8 | "build": "ng build --prod --build-optimizer",
9 | "test": "ng test",
10 | "lint": "ng lint",
11 | "e2e": "ng e2e"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "^5.2.0",
16 | "@angular/cdk": "^5.1.0",
17 | "@angular/common": "^5.2.0",
18 | "@angular/compiler": "^5.2.0",
19 | "@angular/core": "^5.2.0",
20 | "@angular/flex-layout": "^2.0.0-beta.12",
21 | "@angular/forms": "^5.2.0",
22 | "@angular/http": "^5.2.0",
23 | "@angular/material": "^5.1.0",
24 | "@angular/platform-browser": "^5.2.0",
25 | "@angular/platform-browser-dynamic": "^5.2.0",
26 | "@angular/router": "^5.2.0",
27 | "@angular/service-worker": "^5.2.0",
28 | "@swimlane/ngx-charts": "^7.0.1",
29 | "core-js": "^2.4.1",
30 | "d3": "^4.13.0",
31 | "hammerjs": "^2.0.8",
32 | "moment": "^2.20.1",
33 | "rxjs": "^5.5.6",
34 | "sjcl": "^1.0.8",
35 | "zone.js": "^0.8.19"
36 | },
37 | "devDependencies": {
38 | "@angular/cli": "1.6.7",
39 | "@angular/compiler-cli": "^5.2.0",
40 | "@angular/language-service": "^5.2.0",
41 | "@types/jasmine": "~2.8.3",
42 | "@types/jasminewd2": "~2.0.2",
43 | "@types/node": "~6.0.60",
44 | "codelyzer": "^4.0.1",
45 | "jasmine-core": "~2.8.0",
46 | "jasmine-spec-reporter": "~4.2.1",
47 | "karma": "~2.0.0",
48 | "karma-chrome-launcher": "~2.2.0",
49 | "karma-coverage-istanbul-reporter": "^1.2.1",
50 | "karma-jasmine": "~1.1.0",
51 | "karma-jasmine-html-reporter": "^0.2.2",
52 | "protractor": "~5.1.2",
53 | "ts-node": "~4.1.0",
54 | "tslint": "~5.9.1",
55 | "typescript": "~2.5.3"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/pclubiitk/puppy-love/controllers"
7 | "github.com/pclubiitk/puppy-love/db"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | func PuppyRoute(r *gin.Engine, db db.PuppyDb) {
13 |
14 | r.GET("/", func(c *gin.Context) {
15 | c.JSON(http.StatusAccepted, "Hello from the other side!")
16 | })
17 |
18 | controllers.Db = db
19 |
20 | r.GET("/stats", controllers.GetStats)
21 |
22 | // User administration
23 | users := r.Group("/users")
24 | {
25 | users.POST("/login/first", controllers.UserFirst)
26 | users.POST("/data/update/:you", controllers.UserUpdateData)
27 | users.POST("/data/submit/:you", controllers.UserSubmitTrue)
28 | users.POST("/image/update/:you", controllers.UserUpdateImage)
29 | users.POST("/pass/update/:you", controllers.UserSavePass)
30 |
31 | users.GET("/data/info", controllers.UserLoginGet)
32 | users.GET("/data/match/:you", controllers.MatchGet)
33 | users.GET("/get/:id", controllers.UserGet)
34 | users.GET("/mail/:id", controllers.UserMail)
35 | }
36 |
37 | // Listing users
38 | list := r.Group("/list")
39 | {
40 | list.GET("/all", controllers.ListAll)
41 | list.GET("/pubkey", controllers.PubkeyList)
42 | list.GET("/declare", controllers.DeclareList)
43 | }
44 |
45 | // Hearts
46 | hearts := r.Group("/hearts")
47 | {
48 | hearts.GET("/get/:time/:you", controllers.HeartGet)
49 | }
50 |
51 | // Session administration
52 | session := r.Group("/session")
53 | {
54 | session.POST("/login", controllers.SessionLogin)
55 | session.GET("/logout", controllers.SessionLogout)
56 | }
57 |
58 | // Admin
59 | admin := r.Group("/admin")
60 | {
61 | admin.GET("/declare/prepare", controllers.DeclarePrepare)
62 | admin.GET("/user/drop", controllers.UserDelete)
63 | admin.POST("/user/new", controllers.UserNew)
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/service/signup/user.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "gopkg.in/mgo.v2/bson"
5 | )
6 |
7 | type (
8 | // User represents the structure of our resource
9 | User struct {
10 | Id string `json:"_id" bson:"_id"`
11 | Name string `json:"name" bson:"name"`
12 | Email string `json:"email" bson:"email"`
13 | Gender string `json:"gender" bson:"gender"`
14 | Image string `json:"image" bson:"image"`
15 | Pass string `json:"passHash" bson:"passHash"`
16 | PrivK string `json:"privKey" bson:"privKey"`
17 | PubK string `json:"pubKey" bson:"pubKey"`
18 | AuthC string `json:"authCode" bson:"authCode"`
19 | Data string `json:"data" bson:"data"`
20 | Submit bool `json:"submitted" bson:"submitted"`
21 | Matches string `json:"matches" bson:"matches"`
22 | Vote int `json:"voted" bson:"voted"`
23 | Dirty bool `json:"dirty" bson:"dirty"`
24 | }
25 | )
26 |
27 | type PairUpsert struct {
28 | Selector bson.M
29 | Change bson.M
30 | }
31 |
32 | func UpsertEntry(id1 string, id2 string) PairUpsert {
33 | itemId := id1 + "-" + id2
34 | p1 := id1
35 | p2 := id2
36 | if id1 > id2 {
37 | itemId = id2 + "-" + id1
38 | p1 = id2
39 | p2 = id1
40 | }
41 |
42 | _selector := bson.M{"_id": itemId}
43 |
44 | return PairUpsert{
45 | Selector: _selector,
46 | Change: bson.M{"$setOnInsert": bson.M{
47 | "_id": itemId,
48 | "p1": p1,
49 | "p2": p2,
50 | "t0": bson.M{},
51 | "t1": bson.M{},
52 | "r0": "",
53 | "r1": "",
54 | "v0": "",
55 | "v1": "",
56 | "match": false,
57 | }},
58 | }
59 | }
60 |
61 | func NewDeclareTable(id string) PairUpsert {
62 | _selector := bson.M{"_id": id}
63 |
64 | return PairUpsert{
65 | Selector: _selector,
66 | Change: bson.M{"$setOnInsert": bson.M{
67 | "_id": id,
68 | "t0": "",
69 | "t1": "",
70 | "t2": "",
71 | "t3": "",
72 | }},
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/controllers/list.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "log"
5 | "net/http"
6 |
7 | "github.com/pclubiitk/puppy-love/models"
8 |
9 | "github.com/gin-gonic/gin"
10 | "gopkg.in/mgo.v2/bson"
11 | )
12 |
13 | // @AUTH Get user's basic information
14 | // ---------------------------------------
15 | type typeListAll struct {
16 | Id string `json:"_id" bson:"_id"`
17 | Name string `json:"name" bson:"name"`
18 | Email string `json:"email" bson:"email"`
19 | Image string `json:"image" bson:"image"`
20 | }
21 |
22 | func ListAll(c *gin.Context) {
23 | var results []typeListAll
24 |
25 | // Fetch user
26 | if err := Db.GetCollection("user").
27 | Find(bson.M{}).
28 | All(&results); err != nil {
29 |
30 | c.AbortWithStatus(http.StatusNotFound)
31 | log.Print(err)
32 | return
33 | }
34 |
35 | c.JSON(http.StatusAccepted, results)
36 | }
37 |
38 | func PubkeyList(c *gin.Context) {
39 | var query [](struct {
40 | Id string `json:"_id" bson:"_id"`
41 | PK string `json:"pubKey" bson:"pubKey"`
42 | })
43 |
44 | if err := Db.GetCollection("user").
45 | Find(bson.M{"dirty": false}).
46 | All(&query); err != nil {
47 |
48 | c.AbortWithStatus(http.StatusNotFound)
49 | log.Print(err)
50 | return
51 | }
52 |
53 | c.JSON(http.StatusAccepted, query)
54 | }
55 |
56 | func DeclareList(c *gin.Context) {
57 | id, err := SessionId(c)
58 | if err != nil {
59 | c.AbortWithStatus(http.StatusForbidden)
60 | return
61 | }
62 |
63 | var resp models.Declare
64 | if err := Db.GetById("declare", id).One(&resp); err != nil {
65 | c.AbortWithStatus(http.StatusNotFound)
66 | log.Print(err)
67 | return
68 | }
69 |
70 | if resp.Token0 != "" {
71 | resp.Token0 = "1"
72 | }
73 | if resp.Token1 != "" {
74 | resp.Token1 = "1"
75 | }
76 | if resp.Token2 != "" {
77 | resp.Token2 = "1"
78 | }
79 | if resp.Token3 != "" {
80 | resp.Token3 = "1"
81 | }
82 |
83 | c.JSON(http.StatusOK, resp)
84 | }
85 |
--------------------------------------------------------------------------------
/frontend/src/app/components/signup/signup.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, Output } from '@angular/core';
2 | import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
3 |
4 | import { Crypto } from '../../crypto';
5 |
6 | const passwordMatchValidator: ValidatorFn = (g: FormGroup) => {
7 | return g.get('password').value === g.get('password1').value ? null : { mismatch: true };
8 | };
9 |
10 | @Component({
11 | selector: 'puppy-signup',
12 | templateUrl: './signup.component.html',
13 | styleUrls: [ './signup.component.scss' ]
14 | })
15 | export class SignupComponent {
16 |
17 | mailForm: FormGroup;
18 | signupForm: FormGroup;
19 |
20 | @Output()
21 | private mail = new EventEmitter();
22 | @Output()
23 | private signup = new EventEmitter();
24 |
25 | constructor(private fb: FormBuilder) {
26 | // Create Form
27 | this.mailForm = this.fb.group({
28 | roll: ['', Validators.required],
29 | });
30 | this.signupForm = this.fb.group({
31 | roll: ['', Validators.required],
32 | password: ['', Validators.required],
33 | password1: ['', Validators.required],
34 | authCode: ['', Validators.required],
35 | }, { validator: passwordMatchValidator });
36 | }
37 |
38 | onMail() {
39 | this.mail.emit(this.mailForm.value.roll);
40 | }
41 |
42 | onSignup() {
43 | const { authCode, password, roll } = this.signupForm.value;
44 |
45 | const beginData = Crypto.fromJson({
46 | choices: []
47 | });
48 |
49 | const crypto = new Crypto(password);
50 | // const crypto2 = new Crypto(ccpass);
51 |
52 | const passHash = Crypto.hash(Crypto.hash(Crypto.hash(password)));
53 |
54 | crypto.newKey();
55 |
56 | // Store encrypted private key, public key, and encrypted empty data
57 | const body = {
58 | roll,
59 | passHash,
60 | authCode,
61 | privKey: crypto.encryptSym(crypto.serializePriv()),
62 | pubKey: crypto.serializePub(),
63 | // savePass: crypto2.encryptSym(password),
64 | data: crypto.encryptSym(beginData)
65 | };
66 |
67 | this.signup.emit(body);
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/controllers/session.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 | "net/http"
8 |
9 | "github.com/pclubiitk/puppy-love/config"
10 | "github.com/pclubiitk/puppy-love/models"
11 |
12 | "github.com/gin-contrib/sessions"
13 | "github.com/gin-gonic/gin"
14 | )
15 |
16 | type LoginInfo struct {
17 | Username string `json:"username" xml:"username" form:"username"`
18 | Passhash string `json:"password" xml:"password" form:"password"`
19 | }
20 |
21 | func SessionLogin(c *gin.Context) {
22 | session := sessions.Default(c)
23 | if session.Get("Status") != nil {
24 | session.Clear()
25 | session.Save()
26 | }
27 |
28 | u := new(LoginInfo)
29 | if err := c.BindJSON(u); err != nil {
30 | c.AbortWithStatus(http.StatusBadRequest)
31 | log.Println(err)
32 | return
33 | }
34 |
35 | // @TODO @IMPORTANT Move password to env variable
36 | if u.Username == "admin" {
37 | if u.Passhash == config.CfgAdminPass {
38 | session.Set("Status", "login")
39 | session.Set("id", u.Username)
40 | session.Save()
41 | c.String(http.StatusOK,
42 | fmt.Sprintf("Logged in: %s", u.Username))
43 | } else {
44 | SessionLogout(c)
45 | c.String(http.StatusOK, "Invalid username or password")
46 | }
47 | return
48 | }
49 |
50 | user := models.User{}
51 |
52 | // Fetch user
53 | if err := Db.GetById("user", u.Username).One(&user); err != nil {
54 | SessionLogout(c)
55 | c.String(http.StatusNotFound, "Invalid user")
56 | log.Println("Invalid user: " + u.Username)
57 | return
58 | }
59 |
60 | // Check login
61 | if user.Pass == u.Passhash {
62 | session.Set("Status", "login")
63 | session.Set("id", u.Username)
64 | session.Save()
65 | c.JSON(http.StatusOK, gin.H{
66 | "username": u.Username,
67 | })
68 | } else {
69 | SessionLogout(c)
70 | c.AbortWithStatus(http.StatusForbidden)
71 | }
72 | }
73 |
74 | func SessionLogout(c *gin.Context) {
75 | sessions.Default(c).Clear()
76 | sessions.Default(c).Save()
77 | }
78 |
79 | func SessionId(c *gin.Context) (string, error) {
80 | id := sessions.Default(c).Get("id")
81 | if id != nil {
82 | return id.(string), nil
83 | }
84 | return "", errors.New("No such session")
85 | }
86 |
--------------------------------------------------------------------------------
/frontend/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
3 | import { FlexLayoutModule } from '@angular/flex-layout';
4 | import { ReactiveFormsModule } from '@angular/forms';
5 | import { NgModule } from '@angular/core';
6 | import { HttpClientModule } from '@angular/common/http';
7 |
8 | import { NgxChartsModule } from '@swimlane/ngx-charts';
9 |
10 | import { MatButtonModule } from '@angular/material/button';
11 | import { MatCardModule } from '@angular/material/card';
12 | import { MatDividerModule } from '@angular/material/divider';
13 | import { MatChipsModule } from '@angular/material/chips';
14 | import { MatFormFieldModule } from '@angular/material/form-field';
15 | import { MatIconModule } from '@angular/material/icon';
16 | import { MatInputModule } from '@angular/material/input';
17 | import { MatListModule } from '@angular/material/list';
18 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
19 | import { MatSnackBarModule } from '@angular/material/snack-bar';
20 |
21 | import { AppRoutingModule } from './app-routing.module';
22 |
23 | import { AppComponent } from './app.component';
24 |
25 | import { environment } from '../environments/environment';
26 | import { components } from './components';
27 | import { containers } from './containers';
28 | import { MainService } from './services/main.service';
29 | import { LoggedInGuard } from './guards/logged-in.guard';
30 | import { LoggedOutGuard } from './guards/logged-out.guard';
31 |
32 | @NgModule({
33 | declarations: [
34 | AppComponent,
35 | ...components,
36 | ...containers,
37 | ],
38 | imports: [
39 | BrowserModule,
40 | BrowserAnimationsModule,
41 | HttpClientModule,
42 | ReactiveFormsModule,
43 | NgxChartsModule,
44 |
45 | FlexLayoutModule,
46 | MatButtonModule,
47 | MatCardModule,
48 | MatChipsModule,
49 | MatDividerModule,
50 | MatFormFieldModule,
51 | MatIconModule,
52 | MatInputModule,
53 | MatListModule,
54 | MatProgressSpinnerModule,
55 | MatSnackBarModule,
56 |
57 | AppRoutingModule,
58 | ],
59 | providers: [ LoggedInGuard, LoggedOutGuard, MainService ],
60 | bootstrap: [AppComponent]
61 | })
62 | export class AppModule { }
63 |
--------------------------------------------------------------------------------
/service/signup/routines.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/smtp"
6 |
7 | "gopkg.in/mgo.v2"
8 | "gopkg.in/mgo.v2/bson"
9 | )
10 |
11 | func MarkNotDirtyAlt(u User) mgo.Change {
12 | return mgo.Change{
13 | Update: bson.M{"$set": bson.M{
14 | "dirty": false,
15 | }},
16 | ReturnNew: true,
17 | }
18 | }
19 |
20 | func QueueService(listen_channel chan string, signup_channel chan string) {
21 | for request := range listen_channel {
22 | go func(request string) {
23 | signup_channel <- request[1:]
24 | }(request)
25 | }
26 | }
27 |
28 | func SignupService(
29 | Db PuppyDb,
30 | signup_channel chan string,
31 | mail_channel chan User) {
32 |
33 | for id := range signup_channel {
34 | log.Println("Signing up: " + id)
35 |
36 | u := User{}
37 |
38 | // If no such user
39 | if err := Db.GetById("user", id).One(&u); err != nil {
40 | log.Print(err)
41 | continue
42 | }
43 |
44 | // If user has already been computed
45 | if u.Dirty == false {
46 | log.Print("User ", id, " is not dirty. Skipping.")
47 |
48 | // Mailing should be async
49 | go func(user User) {
50 | log.Println("Sending mail now")
51 | mail_channel <- user
52 | }(u)
53 |
54 | continue
55 | }
56 |
57 | // Mark user as not dirty
58 | if _, err := Db.GetById("user", id).
59 | Apply(MarkNotDirtyAlt(u), &u); err != nil {
60 |
61 | log.Println("ERROR: Could not mark ", id, " as not dirty")
62 | log.Println(err)
63 | }
64 |
65 | // Mailing should be async
66 | go func(user User) {
67 | mail_channel <- user
68 | log.Println("Sending mail now")
69 | }(u)
70 | }
71 | }
72 |
73 | func MailerService(Db PuppyDb, mail_channel chan User) {
74 | mailCounter := 0
75 | auth := smtp.PlainAuth("", EmailUser, EmailPass,
76 | EmailHost)
77 |
78 | for u := range mail_channel {
79 | log.Println("Setting up smtp")
80 |
81 | to := []string{u.Email + "@iitk.ac.in"}
82 | msg := []byte("To: " + u.Email + "@iitk.ac.in" + "\r\n" +
83 | "Subject: Puppy-Love authentication code\r\n" +
84 | "\r\n" +
85 | "Use this token while signing up, and don't share it with anyone.\n" +
86 | "Token: " + u.AuthC + "\n" +
87 | ".\r\n")
88 | err := smtp.SendMail(EmailHost+":"+EmailPort, auth,
89 | EmailUser, to, msg)
90 |
91 | if err != nil {
92 | log.Println("ERROR: while mailing user ", u.Email, " ", u.Id)
93 | log.Println(err)
94 | } else {
95 | mailCounter += 1
96 | log.Println("Mailed " + u.Id)
97 | log.Println("Mails sent since inception", mailCounter)
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/frontend/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | // import 'core-js/es6/reflect';
42 |
43 |
44 | /** Evergreen browsers require these. **/
45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
46 | import 'core-js/es7/reflect';
47 |
48 |
49 | /**
50 | * Required to support Web Animations `@angular/platform-browser/animations`.
51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
52 | **/
53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
54 |
55 |
56 |
57 | /***************************************************************************************************
58 | * Zone JS is required by default for Angular itself.
59 | */
60 | import 'zone.js/dist/zone'; // Included with Angular CLI.
61 |
62 |
63 |
64 | /***************************************************************************************************
65 | * APPLICATION IMPORTS
66 | */
67 |
--------------------------------------------------------------------------------
/scripts/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "requires": true,
3 | "lockfileVersion": 1,
4 | "dependencies": {
5 | "bson": {
6 | "version": "1.0.4",
7 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz",
8 | "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw="
9 | },
10 | "camelcase": {
11 | "version": "4.1.0",
12 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
13 | "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
14 | },
15 | "mongodb": {
16 | "version": "3.0.2",
17 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.2.tgz",
18 | "integrity": "sha512-E50FmpSQchZAimn2uPIegoNoH9UQYR1yiGHtQPmmg8/Ekc97w6owHoqaBoz+assnd9V5LxMzmQ/VEWMsQMgZhQ==",
19 | "requires": {
20 | "mongodb-core": "3.0.2"
21 | }
22 | },
23 | "mongodb-core": {
24 | "version": "3.0.2",
25 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.0.2.tgz",
26 | "integrity": "sha512-p1B0qwFQUw6C1OlFJnrOJp8KaX7MuGoogRbTaupRt0y+pPRkMllHWtE9V6i1CDtTvI3/3sy2sQwqWez7zuXEAA==",
27 | "requires": {
28 | "bson": "1.0.4",
29 | "require_optional": "1.0.1"
30 | }
31 | },
32 | "password-generator": {
33 | "version": "2.2.0",
34 | "resolved": "https://registry.npmjs.org/password-generator/-/password-generator-2.2.0.tgz",
35 | "integrity": "sha1-/HXP95URCSPgVKWnFiNDMkC/Xkk=",
36 | "requires": {
37 | "yargs-parser": "8.1.0"
38 | }
39 | },
40 | "require_optional": {
41 | "version": "1.0.1",
42 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
43 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
44 | "requires": {
45 | "resolve-from": "2.0.0",
46 | "semver": "5.5.0"
47 | }
48 | },
49 | "resolve-from": {
50 | "version": "2.0.0",
51 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
52 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
53 | },
54 | "semver": {
55 | "version": "5.5.0",
56 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
57 | "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
58 | },
59 | "yargs-parser": {
60 | "version": "8.1.0",
61 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz",
62 | "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==",
63 | "requires": {
64 | "camelcase": "4.1.0"
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/frontend/src/app/crypto.ts:
--------------------------------------------------------------------------------
1 | export class Crypto {
2 |
3 | static hash(data: string) {
4 | const bits = sjcl.hash.sha256.hash(data);
5 | return sjcl.codec.hex.fromBits(bits);
6 | }
7 |
8 | static toJson(data: string) {
9 | return JSON.parse(data);
10 | }
11 |
12 | static fromJson(data) {
13 | return JSON.stringify(data);
14 | }
15 |
16 | static getRand(cnt = 5): string {
17 | return sjcl.codec.hex.fromBits(sjcl.random.randomWords(cnt));
18 | }
19 |
20 | constructor(private password?: string,
21 | private pubK?: sjcl.SjclElGamalPublicKey,
22 | private priK?: sjcl.SjclElGamalSecretKey) {
23 | }
24 |
25 | // Key creation and storage
26 | // ------------------------
27 | newKey() {
28 | // 6 => Paranoia value
29 | const pair = sjcl.ecc.elGamal.generateKeys(256, 6);
30 | this.priK = pair.sec;
31 | this.pubK = pair.pub;
32 | }
33 |
34 | diffieHellman(pub: string): string {
35 | let pp = new sjcl.ecc.elGamal.publicKey(
36 | sjcl.ecc.curves.c256,
37 | sjcl.codec.base64.toBits(pub)
38 | );
39 | return sjcl.codec.hex.fromBits(this.priK.dh(pp));
40 | }
41 |
42 | serializePub(): string {
43 | const pub = this.pubK.get();
44 | return sjcl.codec.base64.fromBits(pub.x.concat(pub.y));
45 | }
46 |
47 | deserializePub(serializedVal: string) {
48 | this.pubK = new sjcl.ecc.elGamal.publicKey(
49 | sjcl.ecc.curves.c256,
50 | sjcl.codec.base64.toBits(serializedVal)
51 | );
52 | }
53 |
54 | serializePriv(): string {
55 | return sjcl.codec.base64.fromBits(this.priK.get());
56 | }
57 |
58 | deserializePriv(sec: string) {
59 | let bitsec = sjcl.codec.base64.toBits(sec);
60 | this.priK = new sjcl.ecc.elGamal.secretKey(
61 | sjcl.ecc.curves.c256,
62 | sjcl.ecc.curves.c256.field.fromBits(bitsec)
63 | );
64 | }
65 |
66 | // Symmetric enc and dec
67 | // ---------------------
68 | encryptSym(data: string) {
69 | return sjcl.json.encrypt(this.password, data);
70 | }
71 |
72 | decryptSym(data: sjcl.SjclCipherEncrypted) {
73 | try {
74 | return sjcl.json.decrypt(this.password, data) || '{}';
75 | } catch (e) {
76 | return '{}';
77 | }
78 | }
79 |
80 | // Asymmetric enc and dec
81 | // ----------------------
82 | encryptAsym(data: string): sjcl.SjclCipherEncrypted {
83 | if (!this.pubK) {
84 | console.error('Using non-existing public key');
85 | return;
86 | }
87 |
88 | return sjcl.encrypt(this.pubK, data);
89 | }
90 |
91 |
92 | decryptAsym(data: sjcl.SjclCipherEncrypted): string | undefined {
93 | if (!this.priK) {
94 | console.error('Decryption requires private key');
95 | return undefined;
96 | }
97 |
98 | try {
99 | return sjcl.decrypt(this.priK, data);
100 | } catch (e) {
101 | return undefined;
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/frontend/src/app/components/search/search.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
2 | import { MatInput } from '@angular/material/input';
3 | import { Observable } from 'rxjs/Observable';
4 | import { Subject } from 'rxjs/Subject';
5 | import 'rxjs/add/operator/debounceTime';
6 | import 'rxjs/add/operator/distinctUntilChanged';
7 |
8 | @Component({
9 | selector: 'puppy-search',
10 | templateUrl: './search.component.html',
11 | styleUrls: ['./search.component.css']
12 | })
13 | export class SearchComponent implements OnInit {
14 |
15 | @Input()
16 | students: any[];
17 | @ViewChild('searchBox', { read: MatInput }) searchBox: MatInput;
18 | @Output()
19 | select = new EventEmitter();
20 |
21 | result = [];
22 |
23 | private latestTerm = '';
24 | private searchTerms = new Subject();
25 |
26 | constructor() {}
27 |
28 | searchTerm(t: string): void {
29 | const term = t.trim();
30 | this.searchTerms.next(term);
31 | }
32 |
33 | onlyUnique(value: T, index: number, self: Array): boolean {
34 | return self.indexOf(value) === index;
35 | }
36 |
37 | ngOnInit(): void {
38 |
39 | this.searchTerms
40 | .debounceTime(300) // wait 300ms after each keystroke before considering the term
41 | .distinctUntilChanged() // ignore if next search term is same as previous
42 | .subscribe(term => {
43 | this.latestTerm = term;
44 | this.update();
45 | });
46 |
47 | }
48 |
49 | isAny(arr: Array) {
50 | return (arr.length === 0 ||
51 | (arr.length === 1 && arr[0] === 'Any'));
52 | }
53 |
54 | update(): void {
55 | if (this.latestTerm.length > 2) {
56 | this.result = this.getResults(this.students, this.latestTerm);
57 | } else {
58 | this.result = [];
59 | }
60 | }
61 |
62 |
63 | getResults(students: any[], term: string): any[] {
64 |
65 | const escape = (s: string) => {
66 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
67 | };
68 |
69 | const filter = (elem): boolean => {
70 |
71 | if (!(term === null || term === '')) {
72 | const termregex = new RegExp(escape(term).replace(/\s+/g, ' '), 'i');
73 | return (termregex.test(elem._id) || termregex.test(elem.email) || termregex.test(elem.name.replace(/\s+/g, ' ')));
74 | }
75 |
76 | return true;
77 |
78 | };
79 |
80 | // Use forloop instead of filter
81 | // see https://jsperf.com/javascript-filter-vs-loop
82 | // return students.filter(filter);
83 | const resultArray = [];
84 | for (let i = 0; i < students.length; i++) {
85 | const student = students[i];
86 | if (filter(student)) {
87 | resultArray.push(student);
88 | }
89 | }
90 | return resultArray;
91 | }
92 |
93 | onSelect(event: any) {
94 | this.searchTerms.next('');
95 | this.select.emit(event);
96 | (this.searchBox as any)._elementRef.nativeElement.value = '';
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/models/user.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/pclubiitk/puppy-love/utils"
5 |
6 | "gopkg.in/mgo.v2"
7 | "gopkg.in/mgo.v2/bson"
8 | )
9 |
10 | type (
11 | // User represents the structure of our resource
12 | User struct {
13 | Id string `json:"_id" bson:"_id"`
14 | Name string `json:"name" bson:"name"`
15 | Email string `json:"email" bson:"email"`
16 | Gender string `json:"gender" bson:"gender"`
17 | Image string `json:"image" bson:"image"`
18 | Pass string `json:"passHash" bson:"passHash"`
19 | PrivK string `json:"privKey" bson:"privKey"`
20 | PubK string `json:"pubKey" bson:"pubKey"`
21 | AuthC string `json:"authCode" bson:"authCode"`
22 | Data string `json:"data" bson:"data"`
23 | Submit bool `json:"submitted" bson:"submitted"`
24 | Matches string `json:"matches" bson:"matches"`
25 | Vote int `json:"voted" bson:"voted"`
26 | Dirty bool `json:"dirty" bson:"dirty"`
27 | SPass string `json:"savepass" bson:"savepass"`
28 | }
29 | )
30 |
31 | // ----------------------------------------
32 | type TypeUserNew struct {
33 | Id string `json:"roll"`
34 | Name string `json:"name"`
35 | Email string `json:"email"`
36 | Gender string `json:"gender"`
37 | Image string `json:"image"`
38 | PassHash string `json:"passHash"`
39 | }
40 |
41 | func NewUser(info *TypeUserNew) User {
42 | return User{
43 | Id: info.Id,
44 | Name: info.Name,
45 | Email: info.Email,
46 | Gender: info.Gender,
47 | Image: info.Image,
48 | Pass: info.PassHash,
49 | PrivK: "",
50 | PubK: "",
51 | AuthC: utils.RandStringRunes(15),
52 | Data: "",
53 | Submit: false,
54 | Matches: "",
55 | Vote: 0,
56 | Dirty: true,
57 | SPass: "",
58 | }
59 | }
60 |
61 | // ----------------------------------------
62 | type TypeUserFirst struct {
63 | Id string `json:"roll"`
64 | AuthCode string `json:"authCode"`
65 | PassHash string `json:"passHash"`
66 | PubKey string `json:"pubKey"`
67 | PrivKey string `json:"privKey"`
68 | Data string `json:"data"`
69 | }
70 |
71 | func (u User) FirstLogin(info *TypeUserFirst) mgo.Change {
72 | return mgo.Change{
73 | Update: bson.M{"$set": bson.M{
74 | "authCode": "",
75 | "passHash": info.PassHash,
76 | "pubKey": info.PubKey,
77 | "privKey": info.PrivKey,
78 | "data": info.Data,
79 | "matches": "",
80 | }},
81 | ReturnNew: true,
82 | }
83 | }
84 |
85 | // ----------------------------------------
86 | func (u User) ValidPass(pass string) bool {
87 | return u.Pass == pass
88 | }
89 |
90 | func (u User) SetField(field string, value interface{}) mgo.Change {
91 | return mgo.Change{
92 | Update: bson.M{"$set": bson.M{
93 | field: value,
94 | }},
95 | ReturnNew: true,
96 | }
97 | }
98 |
99 | type HeartsAndChoices struct {
100 | Hearts []GotHeart `json:"hearts"`
101 | Tokens Declare `json:"tokens"`
102 | }
103 |
--------------------------------------------------------------------------------
/controllers/stats.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/pclubiitk/puppy-love/models"
9 | "gopkg.in/mgo.v2/bson"
10 | )
11 |
12 | // GetStats returns useful statistics
13 | func GetStats(c *gin.Context) {
14 | var users []models.User
15 | var hearts []models.Heart
16 | if err := Db.GetCollection("user").Find(bson.M{"dirty": false}).All(&users); err != nil {
17 | c.String(http.StatusInternalServerError, "Could not get database info")
18 | return
19 | }
20 | if err := Db.GetCollection("heart").Find(nil).All(&hearts); err != nil {
21 | c.String(http.StatusInternalServerError, "Could not get database info")
22 | return
23 | }
24 |
25 | var y18males, y17males, y16males, y15males, othermales int
26 | var y18females, y17females, y16females, y15females, otherfemales int
27 |
28 | for _, user := range users {
29 | if user.Gender == "1" {
30 | if strings.HasPrefix(user.Id, "18") {
31 | y18males++
32 | } else if strings.HasPrefix(user.Id, "17") {
33 | y17males++
34 | } else if strings.HasPrefix(user.Id, "16") {
35 | y16males++
36 | } else if strings.HasPrefix(user.Id, "15") {
37 | y15males++
38 | } else {
39 | othermales++
40 | }
41 | } else {
42 | if strings.HasPrefix(user.Id, "18") {
43 | y18females++
44 | } else if strings.HasPrefix(user.Id, "17") {
45 | y17females++
46 | } else if strings.HasPrefix(user.Id, "16") {
47 | y16females++
48 | } else if strings.HasPrefix(user.Id, "15") {
49 | y15females++
50 | } else {
51 | otherfemales++
52 | }
53 | }
54 | }
55 |
56 | var y18maleHearts, y17maleHearts, y16maleHearts, y15maleHearts, othermaleHearts int
57 | var y18femaleHearts, y17femaleHearts, y16femaleHearts, y15femaleHearts, otherfemaleHearts int
58 |
59 | for _, heart := range hearts {
60 | if heart.Gender == "1" {
61 | if strings.HasPrefix(heart.Id, "18") {
62 | y18maleHearts++
63 | } else if strings.HasPrefix(heart.Id, "17") {
64 | y17maleHearts++
65 | } else if strings.HasPrefix(heart.Id, "16") {
66 | y16maleHearts++
67 | } else if strings.HasPrefix(heart.Id, "15") {
68 | y15maleHearts++
69 | } else {
70 | othermaleHearts++
71 | }
72 | } else {
73 | if strings.HasPrefix(heart.Id, "18") {
74 | y18femaleHearts++
75 | } else if strings.HasPrefix(heart.Id, "17") {
76 | y17femaleHearts++
77 | } else if strings.HasPrefix(heart.Id, "16") {
78 | y16femaleHearts++
79 | } else if strings.HasPrefix(heart.Id, "15") {
80 | y15femaleHearts++
81 | } else {
82 | otherfemaleHearts++
83 | }
84 | }
85 | }
86 |
87 | c.JSON(http.StatusOK, gin.H{
88 | "users": len(users),
89 | "y18males": y18males,
90 | "y17males": y17males,
91 | "y16males": y16males,
92 | "y15males": y15males,
93 | "othermales": othermales,
94 | "y18females": y18females,
95 | "y17females": y17females,
96 | "y16females": y16females,
97 | "y15females": y15females,
98 | "otherfemales": otherfemales,
99 | "y18maleHearts": y18maleHearts,
100 | "y17maleHearts": y17maleHearts,
101 | "y16maleHearts": y16maleHearts,
102 | "y15maleHearts": y15maleHearts,
103 | "othermaleHearts": othermaleHearts,
104 | "y18femaleHearts": y18femaleHearts,
105 | "y17femaleHearts": y17femaleHearts,
106 | "y16femaleHearts": y16femaleHearts,
107 | "y15femaleHearts": y15femaleHearts,
108 | "otherfemaleHearts": otherfemaleHearts,
109 | })
110 | }
111 |
--------------------------------------------------------------------------------
/frontend/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs",
22 | "rxjs/Rx"
23 | ],
24 | "import-spacing": true,
25 | "indent": [
26 | true,
27 | "spaces"
28 | ],
29 | "interface-over-type-literal": true,
30 | "label-position": true,
31 | "max-line-length": [
32 | true,
33 | 140
34 | ],
35 | "member-access": false,
36 | "member-ordering": [
37 | true,
38 | {
39 | "order": [
40 | "static-field",
41 | "instance-field",
42 | "static-method",
43 | "instance-method"
44 | ]
45 | }
46 | ],
47 | "no-arg": true,
48 | "no-bitwise": true,
49 | "no-console": [
50 | true,
51 | "debug",
52 | "info",
53 | "time",
54 | "timeEnd",
55 | "trace"
56 | ],
57 | "no-construct": true,
58 | "no-debugger": true,
59 | "no-duplicate-super": true,
60 | "no-empty": false,
61 | "no-empty-interface": true,
62 | "no-eval": true,
63 | "no-inferrable-types": [
64 | true,
65 | "ignore-params"
66 | ],
67 | "no-misused-new": true,
68 | "no-non-null-assertion": true,
69 | "no-shadowed-variable": true,
70 | "no-string-literal": false,
71 | "no-string-throw": true,
72 | "no-switch-case-fall-through": true,
73 | "no-trailing-whitespace": true,
74 | "no-unnecessary-initializer": true,
75 | "no-unused-expression": true,
76 | "no-use-before-declare": true,
77 | "no-var-keyword": true,
78 | "object-literal-sort-keys": false,
79 | "one-line": [
80 | true,
81 | "check-open-brace",
82 | "check-catch",
83 | "check-else",
84 | "check-whitespace"
85 | ],
86 | "prefer-const": true,
87 | "quotemark": [
88 | true,
89 | "single"
90 | ],
91 | "radix": true,
92 | "semicolon": [
93 | true,
94 | "always"
95 | ],
96 | "triple-equals": [
97 | true,
98 | "allow-null-check"
99 | ],
100 | "typedef-whitespace": [
101 | true,
102 | {
103 | "call-signature": "nospace",
104 | "index-signature": "nospace",
105 | "parameter": "nospace",
106 | "property-declaration": "nospace",
107 | "variable-declaration": "nospace"
108 | }
109 | ],
110 | "unified-signatures": true,
111 | "variable-name": false,
112 | "whitespace": [
113 | true,
114 | "check-branch",
115 | "check-decl",
116 | "check-operator",
117 | "check-separator",
118 | "check-type"
119 | ],
120 | "directive-selector": [
121 | true,
122 | "attribute",
123 | "puppy",
124 | "camelCase"
125 | ],
126 | "component-selector": [
127 | true,
128 | "element",
129 | "puppy",
130 | "kebab-case"
131 | ],
132 | "no-output-on-prefix": true,
133 | "use-input-property-decorator": true,
134 | "use-output-property-decorator": true,
135 | "use-host-property-decorator": true,
136 | "no-input-rename": true,
137 | "no-output-rename": true,
138 | "use-life-cycle-interface": true,
139 | "use-pipe-transform-interface": true,
140 | "component-class-suffix": true,
141 | "directive-class-suffix": true
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/frontend/src/app/components/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { DomSanitizer } from '@angular/platform-browser';
3 | import { MatChipInputEvent } from '@angular/material';
4 | import { MatSnackBar } from '@angular/material/snack-bar';
5 | import { ENTER, COMMA } from '@angular/cdk/keycodes';
6 |
7 | import { MainService } from '../../services/main.service';
8 |
9 | function ImageURL(rollnum: string, userid: string) {
10 | const iitkhome = `http://home.iitk.ac.in/~${ userid }/dp`;
11 | const oaimage = `https://oa.cc.iitk.ac.in/Oa/Jsp/Photo/${ rollnum }_0.jpg`;
12 | return `url("${ iitkhome }"), url("${ oaimage }")`;
13 | }
14 |
15 | @Component({
16 | selector: 'puppy-home',
17 | templateUrl: './home.component.html',
18 | styleUrls: [ './home.component.scss' ]
19 | })
20 | export class HomeComponent implements OnInit {
21 |
22 | // Enter, comma
23 | separatorKeysCodes = [ENTER, COMMA];
24 |
25 | user$: any;
26 |
27 | constructor(private main: MainService,
28 | private sanitizer: DomSanitizer,
29 | private snackbar: MatSnackBar) {}
30 |
31 | ngOnInit() {
32 | this.user$ = this.main.user$;
33 | this.doSubmit();
34 | }
35 |
36 | get url() {
37 | const currentUser = {
38 | ...this.main.user$.value
39 | };
40 | return this.sanitizer.bypassSecurityTrustStyle(ImageURL(currentUser._id, currentUser.email));
41 | }
42 |
43 | maleHearts(user) {
44 | return user.data.received.filter((x) => x.genderOfSender === '1');
45 | }
46 |
47 | femaleHearts(user) {
48 | return user.data.received.filter((x) => x.genderOfSender === '0');
49 | }
50 |
51 | add(event): void {
52 | const currentUser = {
53 | ...this.main.user$.value
54 | };
55 |
56 | if (!event) {
57 | return;
58 | }
59 |
60 | if (event._id !== currentUser._id && !currentUser.data.choices.some((x) => x._id === event._id)) {
61 | currentUser.data.choices.push(event);
62 | this.main.user$.next(currentUser);
63 | }
64 | }
65 |
66 | // add(event: MatChipInputEvent): void {
67 | // const input = event.input;
68 | // const value = event.value;
69 |
70 | // const currentUser = {
71 | // ...this.main.user$.value
72 | // };
73 | // if ((value || '').trim()) {
74 | // currentUser.data.choices.push({ _id: value.trim(), name: 'Foobar', email: 'foobar' });
75 | // this.main.user$.next(currentUser);
76 | // }
77 |
78 | // // Reset the input value
79 | // if (input) {
80 | // input.value = '';
81 | // }
82 | // }
83 |
84 | remove(fruit: any): void {
85 | const currentUser = {
86 | ...this.main.user$.value
87 | };
88 |
89 | if (currentUser.submitted) {
90 | return;
91 | }
92 |
93 | const index = currentUser.data.choices.indexOf(fruit);
94 |
95 | if (index >= 0) {
96 | currentUser.data.choices.splice(index, 1);
97 | this.main.user$.next(currentUser);
98 | }
99 | }
100 |
101 | doSubmit() {
102 | const user = this.user$.value;
103 | if (user.submitted) {
104 | this.main.submit().subscribe(
105 | () => console.log('Autosubmission.'),
106 | (error) => this.snackbar.open('An error occurred: ' + error, '', { duration: 3000 })
107 | );
108 | }
109 | }
110 |
111 | onSubmit() {
112 | if(!window.confirm('This will finalize your choices, you cannot change them afterwards. Proceed?')) {
113 | // If you've seen this, you can assume that you've understood all the code here.
114 | return;
115 | }
116 | this.snackbar.open('Submitting, please wait...');
117 | this.main.submit().subscribe(
118 | () => this.snackbar.open('Submitted.', '', { duration: 3000 }),
119 | (error) => this.snackbar.open('An error occurred: ' + error, '', { duration: 3000 })
120 | );
121 | }
122 |
123 | onSave() {
124 | this.snackbar.open('Saving your info, please wait...');
125 | this.main.save().then(() => this.snackbar.open('Saved your info.', '', { duration: 3000 }));
126 | }
127 |
128 | onLogout() {
129 | location.reload();
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [[projects]]
5 | name = "github.com/boj/redistore"
6 | packages = ["."]
7 | revision = "fc113767cd6b051980f260d6dbe84b2740c46ab0"
8 | version = "v1.2"
9 |
10 | [[projects]]
11 | branch = "master"
12 | name = "github.com/bradfitz/gomemcache"
13 | packages = ["memcache"]
14 | revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b"
15 |
16 | [[projects]]
17 | branch = "master"
18 | name = "github.com/bradleypeabody/gorilla-sessions-memcache"
19 | packages = ["."]
20 | revision = "75ee37df8664a47cd5d9eb84d41e1f2b08086893"
21 |
22 | [[projects]]
23 | name = "github.com/garyburd/redigo"
24 | packages = [
25 | "internal",
26 | "redis"
27 | ]
28 | revision = "d1ed5c67e5794de818ea85e6b522fda02623a484"
29 | version = "v1.4.0"
30 |
31 | [[projects]]
32 | branch = "master"
33 | name = "github.com/gin-contrib/sessions"
34 | packages = ["."]
35 | revision = "cccdeef56346e7037ca92de250c2b55ef5e7ffe3"
36 |
37 | [[projects]]
38 | branch = "master"
39 | name = "github.com/gin-contrib/sse"
40 | packages = ["."]
41 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
42 |
43 | [[projects]]
44 | name = "github.com/gin-gonic/gin"
45 | packages = [
46 | ".",
47 | "binding",
48 | "render"
49 | ]
50 | revision = "d459835d2b077e44f7c9b453505ee29881d5d12d"
51 | version = "v1.2"
52 |
53 | [[projects]]
54 | branch = "master"
55 | name = "github.com/golang/protobuf"
56 | packages = ["proto"]
57 | revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
58 |
59 | [[projects]]
60 | name = "github.com/gorilla/context"
61 | packages = ["."]
62 | revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
63 | version = "v1.1"
64 |
65 | [[projects]]
66 | name = "github.com/gorilla/securecookie"
67 | packages = ["."]
68 | revision = "667fe4e3466a040b780561fe9b51a83a3753eefc"
69 | version = "v1.1"
70 |
71 | [[projects]]
72 | name = "github.com/gorilla/sessions"
73 | packages = ["."]
74 | revision = "ca9ada44574153444b00d3fd9c8559e4cc95f896"
75 | version = "v1.1"
76 |
77 | [[projects]]
78 | name = "github.com/imdario/mergo"
79 | packages = ["."]
80 | revision = "163f41321a19dd09362d4c63cc2489db2015f1f4"
81 | version = "0.3.2"
82 |
83 | [[projects]]
84 | name = "github.com/kataras/go-errors"
85 | packages = ["."]
86 | revision = "d2aaffdebcb40c69c4cb6a95e436430cde192435"
87 | version = "v0.0.3"
88 |
89 | [[projects]]
90 | branch = "master"
91 | name = "github.com/kidstuff/mongostore"
92 | packages = ["."]
93 | revision = "256d65ac5b0e35e7c5ebb3f175c0bed1e5c2b253"
94 |
95 | [[projects]]
96 | name = "github.com/mattn/go-isatty"
97 | packages = ["."]
98 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
99 | version = "v0.0.3"
100 |
101 | [[projects]]
102 | branch = "master"
103 | name = "github.com/ugorji/go"
104 | packages = ["codec"]
105 | revision = "9831f2c3ac1068a78f50999a30db84270f647af6"
106 |
107 | [[projects]]
108 | branch = "master"
109 | name = "golang.org/x/sys"
110 | packages = ["unix"]
111 | revision = "2c42eef0765b9837fbdab12011af7830f55f88f0"
112 |
113 | [[projects]]
114 | name = "gopkg.in/go-playground/validator.v8"
115 | packages = ["."]
116 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
117 | version = "v8.18.2"
118 |
119 | [[projects]]
120 | name = "gopkg.in/kataras/go-sessions.v0"
121 | packages = [
122 | "sessiondb/redis",
123 | "sessiondb/redis/service"
124 | ]
125 | revision = "d7ae81e3f9a643facd6f210f3bb1dc3674302b10"
126 | version = "v0.0.7"
127 |
128 | [[projects]]
129 | branch = "v2"
130 | name = "gopkg.in/mgo.v2"
131 | packages = [
132 | ".",
133 | "bson",
134 | "internal/json",
135 | "internal/sasl",
136 | "internal/scram"
137 | ]
138 | revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655"
139 |
140 | [[projects]]
141 | branch = "v2"
142 | name = "gopkg.in/yaml.v2"
143 | packages = ["."]
144 | revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4"
145 |
146 | [solve-meta]
147 | analyzer-name = "dep"
148 | analyzer-version = 1
149 | inputs-digest = "7912ee9fb8066fad398d9fc2c87f8ba0d66badbf628e7443c8f37c42667c097b"
150 | solver-name = "gps-cdcl"
151 | solver-version = 1
152 |
--------------------------------------------------------------------------------
/frontend/src/app/components/home/home.component.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | Hello, {{ user.name }}
8 |
9 |
10 |
11 |
12 |
13 | Hi {{ user.name }}
14 |
15 |
16 | {{ user.submitted ? 'check' : 'close' }}
17 | {{ user.submitted ? 'Submitted!' : 'Not Submitted' }}
18 |
19 |
20 | power_settings_new
21 | Logout
22 |
23 |
24 | About the platform
25 | Source Code
26 | pclub.in
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 | = 4 && !user.submitted">
37 |
You can only select 4 people.
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
Your choices:
56 |
(Click to remove)
57 |
58 |
61 |
62 | save
63 | Save your choices!
64 |
65 |
66 | sentiment_very_satisfied
67 | Lock and submit!
68 |
69 |
70 |
71 |
Hearts from people that the OA identifies as male:
72 |
73 |
74 |
75 |
76 |
77 |
Hearts from people that the OA identifies as female:
78 |
79 |
80 |
81 |
82 |
84 |
Sorry, you haven't gotten any hearts (yet).
85 |
True love comes to those who wait.
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | Loading
95 |
96 |
--------------------------------------------------------------------------------
/frontend/src/app/components/results/results.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { DomSanitizer } from '@angular/platform-browser';
3 | import { MatSnackBar } from '@angular/material/snack-bar';
4 |
5 | import { of } from 'rxjs/observable/of';
6 | import { catchError, switchMap } from 'rxjs/operators';
7 | import { MainService, Stats } from '../../services/main.service';
8 |
9 | function ImageURL(rollnum: string, userid: string) {
10 | const iitkhome = `http://home.iitk.ac.in/~${ userid }/dp`;
11 | const oaimage = `https://oa.cc.iitk.ac.in/Oa/Jsp/Photo/${ rollnum }_0.jpg`;
12 | return `url("${ iitkhome }"), url("${ oaimage }")`;
13 | }
14 |
15 | @Component({
16 | selector: 'puppy-results',
17 | templateUrl: './results.component.html',
18 | styleUrls: [ './results.component.scss' ]
19 | })
20 | export class ResultsComponent implements OnInit {
21 |
22 | user$: any;
23 | matches: any[];
24 | stats: Stats;
25 |
26 | colorScheme = {
27 | domain: ['#C2024F', '#04BBBF', '#D2D945', '#FCB13F', '#FF594F']
28 | };
29 |
30 | constructor(private main: MainService,
31 | private sanitizer: DomSanitizer,
32 | private snackbar: MatSnackBar) {}
33 |
34 | ngOnInit() {
35 | this.user$ = this.main.user$;
36 | this.doSubmit();
37 | this.getStats();
38 | }
39 |
40 | get url() {
41 | const currentUser = {
42 | ...this.main.user$.value
43 | };
44 | return this.sanitizer.bypassSecurityTrustStyle(ImageURL(currentUser._id, currentUser.email));
45 | }
46 |
47 | get registrations() {
48 | const stats = this.stats;
49 | const totalMales = stats.othermales + stats.y15males + stats.y16males + stats.y17males + stats.y18males;
50 | const totalFemales = stats.otherfemales + stats.y15females + stats.y16females + stats.y17females + stats.y18females;
51 | return [{
52 | name: 'Males',
53 | value: totalMales,
54 | }, {
55 | name: 'Females',
56 | value: totalFemales,
57 | }];
58 | }
59 |
60 | get hearts() {
61 | const stats = this.stats;
62 | const totalMaleHearts = stats.othermaleHearts + stats.y15maleHearts + stats.y16maleHearts + stats.y17maleHearts + stats.y18maleHearts;
63 | const totalFemaleHearts = stats.otherfemaleHearts + stats.y15femaleHearts + stats.y16femaleHearts + stats.y17femaleHearts + stats.y18femaleHearts;
64 | return [{
65 | name: 'Males',
66 | value: totalMaleHearts,
67 | }, {
68 | name: 'Females',
69 | value: totalFemaleHearts,
70 | }];
71 | }
72 | get fhearts() {
73 | const stats = this.stats;
74 | return [{
75 | name: 'Others',
76 | value: stats.otherfemaleHearts,
77 | }, {
78 | name: 'Y15',
79 | value: stats.y15femaleHearts,
80 | }, {
81 | name: 'Y16',
82 | value: stats.y16femaleHearts,
83 | }, {
84 | name: 'Y17',
85 | value: stats.y17femaleHearts,
86 | }, {
87 | name: 'Y18',
88 | value: stats.y18femaleHearts,
89 | }].reverse();
90 |
91 | }
92 |
93 | get mhearts() {
94 | const stats = this.stats;
95 | return [{
96 | name: 'Others',
97 | value: stats.othermaleHearts,
98 | }, {
99 | name: 'Y15',
100 | value: stats.y15maleHearts,
101 | }, {
102 | name: 'Y16',
103 | value: stats.y16maleHearts,
104 | }, {
105 | name: 'Y17',
106 | value: stats.y17maleHearts,
107 | }, {
108 | name: 'Y18',
109 | value: stats.y18maleHearts,
110 | }].reverse();
111 | }
112 |
113 | get fregs() {
114 | const stats = this.stats;
115 | return [{
116 | name: 'Others',
117 | value: stats.otherfemales,
118 | }, {
119 | name: 'Y15',
120 | value: stats.y15females,
121 | }, {
122 | name: 'Y16',
123 | value: stats.y16females,
124 | }, {
125 | name: 'Y17',
126 | value: stats.y17females,
127 | }, {
128 | name: 'Y18',
129 | value: stats.y18females,
130 | }].reverse();
131 |
132 | }
133 |
134 | get mregs() {
135 | const stats = this.stats;
136 | return [{
137 | name: 'Others',
138 | value: stats.othermales,
139 | }, {
140 | name: 'Y15',
141 | value: stats.y15males,
142 | }, {
143 | name: 'Y16',
144 | value: stats.y16males,
145 | }, {
146 | name: 'Y17',
147 | value: stats.y17males,
148 | }, {
149 | name: 'Y18',
150 | value: stats.y18males,
151 | }].reverse();
152 | }
153 |
154 | maleHearts(user) {
155 | return user.data.received.filter((x) => x.genderOfSender === '1');
156 | }
157 |
158 | femaleHearts(user) {
159 | return user.data.received.filter((x) => x.genderOfSender === '0');
160 | }
161 |
162 | doSubmit() {
163 | const user = this.user$.value;
164 | this.main.submit().pipe(
165 | catchError((err) => of(console.error(err))),
166 | switchMap(() => this.main.matches()),
167 | ).subscribe(
168 | (match) => {
169 | if (match.matches === '') {
170 | this.matches = [];
171 | } else {
172 | this.matches = match.matches.split(' ').map(x => this.main.people.filter(p => p._id === x)[0]);
173 | }
174 | },
175 | (error) => this.snackbar.open(error, '', { duration: 3000 })
176 | );
177 | }
178 |
179 | getStats() {
180 | this.main.stats().subscribe((x) => this.stats = x);
181 | }
182 |
183 | onLogout() {
184 | location.reload();
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/service/results/results.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "sort"
8 | "strings"
9 |
10 | "gopkg.in/mgo.v2/bson"
11 | )
12 |
13 | type Declare struct {
14 | Id string `json:"_id" bson:"_id"`
15 | Token0 string `json:"t0" bson:"t0"`
16 | Token1 string `json:"t1" bson:"t1"`
17 | Token2 string `json:"t2" bson:"t2"`
18 | Token3 string `json:"t3" bson:"t3"`
19 | }
20 |
21 | type Flat struct {
22 | Id string `json:"_id" bson:"_id"`
23 | Token string `json:"t" bson:"t"`
24 | }
25 |
26 | type ByValue []Flat
27 |
28 | func (b ByValue) Len() int {
29 | return len(b)
30 | }
31 | func (b ByValue) Swap(i, j int) {
32 | b[i], b[j] = b[j], b[i]
33 | }
34 | func (b ByValue) Less(i, j int) bool {
35 | return b[i].Token < b[j].Token
36 | }
37 |
38 | func AddMatch(toAdd string, prev string) string {
39 | if prev == "" {
40 | return toAdd
41 | }
42 |
43 | return prev + " " + toAdd
44 | }
45 |
46 | func main() {
47 | CfgInit()
48 | log.Println("Computing for table:", CfgTable)
49 | db, error := MongoConnect()
50 | if error != nil {
51 | log.Print("ERROR: Could not connect to MongoDB")
52 | os.Exit(1)
53 | }
54 |
55 | var decs []Declare
56 | collection := db.GetCollection(CfgTable)
57 | err := collection.Find(bson.M{}).All(&decs)
58 | if err != nil {
59 | log.Println("ERROR: Results cannot fetch declare list: ")
60 | log.Println(err)
61 | return
62 | }
63 |
64 | var flat []Flat
65 | for _, fe := range decs {
66 | if fe.Token0 != "" {
67 | flat = append(flat, Flat{
68 | Id: fe.Id,
69 | Token: fe.Token0,
70 | })
71 | }
72 | if fe.Token1 != "" {
73 | flat = append(flat, Flat{
74 | Id: fe.Id,
75 | Token: fe.Token1,
76 | })
77 | }
78 | if fe.Token2 != "" {
79 | flat = append(flat, Flat{
80 | Id: fe.Id,
81 | Token: fe.Token2,
82 | })
83 | }
84 | if fe.Token3 != "" {
85 | flat = append(flat, Flat{
86 | Id: fe.Id,
87 | Token: fe.Token3,
88 | })
89 | }
90 | }
91 |
92 | sort.Sort(ByValue(flat))
93 |
94 | for i := range flat {
95 | fmt.Println(flat[i].Token, flat[i].Id)
96 | }
97 |
98 | fmt.Println()
99 |
100 | var emails = []string{}
101 | var maleMale, femaleFemale, crossGender int
102 | var matches string
103 |
104 | for i := range flat {
105 | if i == 0 {
106 | continue
107 | }
108 |
109 | if flat[i].Token == flat[i-1].Token {
110 | fmt.Println("Matched:", flat[i].Id, "and", flat[i-1].Id)
111 |
112 | u1 := User{}
113 | u2 := User{}
114 | if err := db.GetById("user", flat[i].Id).One(&u1); err != nil {
115 | fmt.Println("Error")
116 | fmt.Println(err)
117 | continue
118 | }
119 | if err := db.GetById("user", flat[i-1].Id).One(&u2); err != nil {
120 | fmt.Println("Error")
121 | fmt.Println(err)
122 | continue
123 | }
124 |
125 | fmt.Println("Names: ", u1.Name, "and", u2.Name)
126 | fmt.Println("Emails: ", u1.Email, "and", u2.Email)
127 | fmt.Println("Genders: ", u1.Gender, u2.Gender)
128 | fmt.Println("Prev matches: ", u1.Matches, "; ", u2.Matches)
129 |
130 | // EXTRA STUFF, NOT NEEDED
131 | // If you want to verify some information
132 | // As an example here, I match the hash, in the hypothetical scenario
133 | // that the hash can be computed on the server
134 |
135 | //fmt.Println("Hash: ", flat[i].Token)
136 | //pairId := ""
137 | //if flat[i].Id > flat[i-1].Id {
138 | //pairId = flat[i].Id + "-" + flat[i-1].Id
139 | //} else {
140 | //pairId = flat[i-1].Id + "-" + flat[i].Id
141 | //}
142 |
143 | //out, _ := exec.Command("/bin/bash", "-c", "echo -n '"+pairId+"' | sha256sum").Output()
144 | //h2 := strings.Fields(string(out[:]))[0]
145 | //fmt.Println("Has2: ", h2)
146 | //if h2 != flat[i].Token {
147 | //fmt.Println("ERROR")
148 | //continue
149 | //} else {
150 | //cnt = cnt + 1
151 | //}
152 |
153 | // if u1.Gender == u2.Gender {
154 | // fmt.Println("SAME GENDER")
155 | // fmt.Println("ERROR")
156 | // continue
157 | // }
158 |
159 | if u1.Gender != u2.Gender {
160 | crossGender++
161 | } else if u1.Gender == "1" {
162 | maleMale++
163 | } else {
164 | femaleFemale++
165 | }
166 |
167 | matches += fmt.Sprintf("%s(%s) <-> %s(%s)\n", u1.Name, u1.Id, u2.Name, u2.Id)
168 |
169 | emails = append(emails, u1.Email)
170 | emails = append(emails, u2.Email)
171 |
172 | var b bool
173 | var ans string
174 |
175 | p1matches := strings.Fields(u1.Matches)
176 | b = false
177 | for _, match := range p1matches {
178 | if match == u2.Id {
179 | b = true
180 | break
181 | }
182 | }
183 | if !b {
184 | fmt.Println(u2.Name, "is not in matches of", u1.Name, ". Add?")
185 | // _, _ = fmt.Scanf("%s", &ans)
186 | ans = "y"
187 | if ans == "y" {
188 | fmt.Println("Adding", AddMatch(u2.Id, u1.Matches))
189 |
190 | if _, err := db.GetById("user", u1.Id).
191 | Apply(u1.MatchedUpdate(AddMatch(u2.Id, u1.Matches)), &u1); err != nil {
192 | fmt.Println(err)
193 | }
194 |
195 | } else {
196 | fmt.Println("You said no")
197 | }
198 | }
199 |
200 | p2matches := strings.Fields(u2.Matches)
201 | b = false
202 | for _, match := range p2matches {
203 | if match == u1.Id {
204 | b = true
205 | break
206 | }
207 | }
208 | if !b {
209 | fmt.Println(u1.Name, "is not in matches of", u2.Name, ". Add?")
210 | // _, _ = fmt.Scanf("%s", &ans)
211 | ans = "y"
212 | if ans == "y" {
213 | fmt.Println("Adding", AddMatch(u1.Id, u2.Matches))
214 |
215 | if _, err := db.GetById("user", u2.Id).
216 | Apply(u2.MatchedUpdate(AddMatch(u1.Id, u2.Matches)), &u2); err != nil {
217 | fmt.Println(err)
218 | }
219 |
220 | } else {
221 | fmt.Println("You said no")
222 | }
223 | }
224 | fmt.Println("**********************************************")
225 | }
226 | }
227 | fmt.Println()
228 | for _, mail := range emails {
229 | fmt.Println(mail)
230 | }
231 | fmt.Println(matches)
232 | fmt.Println("Total", maleMale+femaleFemale+crossGender, "matches")
233 | fmt.Println("Male-Male matches: ", maleMale)
234 | fmt.Println("Female-Female matches: ", femaleFemale)
235 | fmt.Println("Male-Female matches: ", crossGender)
236 | fmt.Println()
237 | }
238 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Puppy Love
2 | ==========
3 |
4 | The modern and better avatar of Programming Club's [puppy-love](https://github.com/pclubiitk/valentine).
5 |
6 | Puppy Love is a platform for shy people to reach out anonymously to their crush. They can sign up and mark up to 4 of their crushes. On Valentine's day, the platform will match them anonymously, and will inform them if both of them like each other.
7 |
8 | Used in IIT Kanpur from 7th to 14th February, 2017, witnessing 1800+ registrants.
9 |
10 | 
11 |
12 | Algorithm designed from the ground up, with a completely secure computation model which guarantees the following:
13 |
14 | * The identities of your choices are never, *ever*, exposed in plain text. Not even at the server.
15 | * The server, even while matching couples, can *not* know what the choices were.
16 | * The other person will only know whether you liked him/her or not if that person liked you as well.
17 | * The server will know whether you matched with some person or not, but no more.
18 | * The above guarantees are independent of the code running on the server, and can be verified on the browser.
19 |
20 | Blog posts describing the algorithm:
21 | * [Part 1 - Introduction](https://sakshamsharma.com/2016/10/puppy1/)
22 | * [Part 2 - Matching choices](https://sakshamsharma.com/2016/11/puppy2/)
23 | * [Part 3 - Restricting number of choices to 4 anonymously](https://sakshamsharma.com/2016/12/puppy3/)
24 |
25 | Implementation using:
26 |
27 | * Golang (iris)
28 | * TypeScript
29 | * Angular2
30 | * Bootstrap
31 | * Docker
32 | * Docker Compose
33 |
34 | # Requirements
35 | * golang
36 | * NPM
37 | * Nginx
38 | * Docker
39 |
40 | # Installation / Setup
41 |
42 | ## Server deployment
43 | It is really easy. Like, **really**.
44 |
45 | ```
46 | # You need docker-compose 1.10 or higher.
47 | # Use the release script version from their website for installation.
48 | docker-compose up
49 |
50 | # Use the following to stop a container
51 | docker-compose stop
52 |
53 | # Use the following to start a container
54 | docker-compose start
55 |
56 | # Use the following to recreate and run a container
57 | docker-compose up -d
58 | ```
59 |
60 | ## Development environment setup
61 | You shall need docker, golang, nodejs, nginx and npm for the following steps.
62 |
63 | **Note**: Arch users often have gccgo installed. Please use the package `go` from the main repositories instead.
64 |
65 | ```
66 | mkdir -p $HOME/go/src
67 |
68 | # Change .bashrc to .zshrc depending on your shell
69 | echo "export GOPATH=$HOME/go:$GOPATH" >> $HOME/.bashrc
70 | source $HOME/.bashrc
71 | ```
72 |
73 | Get the source code.
74 |
75 | You can later symlink the following folder into a convenient location.
76 | Just make sure that the actual folder (not symlink) is in the go directory.
77 | ```
78 | git clone https://github.com/pclubiitk/puppy-love $HOME/go/src/github.com/pclubiitk/puppy-love
79 | ```
80 |
81 | Install glide
82 | We use `glide` to maintain dependencies. `go get` is not recommended.
83 | ```
84 | curl https://glide.sh/get | sh
85 | cd $HOME/go/src/github.com/pclubiitk/puppy-love
86 | ```
87 |
88 | Set up nginx
89 | ```
90 | sudo cp puppy.nginx.conf /etc/nginx/sites_enabled/
91 | ```
92 |
93 | Edit /etc/hosts file
94 | ```
95 | # Map dev.puppy.pclub.in to 127.0.0.1
96 | # It should have a line saying:
97 | # 127.0.0.1 dev.puppy.pclub.in
98 | ```
99 |
100 | Get the essential dockers
101 | ```
102 | sudo systemctl start docker
103 | docker run --name puppy-redis -p 6379:6379 -d redis
104 | docker run --name puppy-mongo-db -p 27017:27017 -d mongo
105 |
106 | # Optional (in place of the above command):
107 | # docker run --name puppy-mongo-db -p 27017:27017 -v $HOME/.mongodata:/data/db -d mongo
108 | ```
109 |
110 | Get dependencies for backend
111 | ```
112 | glide install
113 | ```
114 |
115 | Get dependencies for frontend
116 | ```
117 | cd views
118 | sudo npm install -g yarn
119 | yarn install
120 | ```
121 |
122 | ## Run services
123 | ```
124 | sudo systemctl start nginx
125 |
126 | # These are not required if you just finished the above steps
127 | docker start puppy-mongo-db
128 | docker start puppy-redis
129 | ```
130 |
131 | ## Run puppy-love
132 | ### Frontend
133 | ```
134 | # Run frontend (inside folder views)
135 | yarn start
136 |
137 | # IFF production, use
138 | yarn build && python -m http.serve 8091
139 | ```
140 |
141 | ### Backend
142 | ```
143 | # Build (inside puppy-love folder)
144 | go build
145 |
146 | # Optional: These are needed for email verification to work
147 | export EMAIL_USER=
148 | export EMAIL_PASS=
149 | ./puppy-love
150 | ```
151 |
152 | # Setting up basic services
153 | ### Log in
154 | You should first log in as admin. A simple way to do that is the following:
155 | ```
156 | cd scripts
157 | . ./login.sh admin passhash
158 | # Use curl / http normally, but use $CADMIN cookie at the end of your command
159 | # Example: http get 'localhost:3000/admin/...' $CADMIN
160 | ```
161 |
162 | ### Add a user
163 | Note: This requires you to be logged in as $CADMIN
164 | ```
165 | cd scripts
166 | ./newuser.sh
167 | # Follow the commands
168 | # For testing, use your own IITK email address for all users
169 | ```
170 |
171 | ### Set up compute table
172 | Do this AFTER setting up all users. Whenever you add a new user, this has to be run.
173 | ```
174 | http get 'localhost:3000/compute/prepare' $CADMIN
175 | ```
176 |
177 | ### Using the frontend
178 | Once you've created the users, you will need to register them. Open the UI at dev.puppy.pclub.in, and go to signup. You can only register for users which you have created. Get your auth token via email, and then fill up the remaining fields.
179 |
180 | ### Notes
181 | * You cannot login as admin on the frontend UI.
182 | * You can also check mongoDB's data for the auth token for the user.
183 | * Doing the above will mandate marking the user as `non-dirty` in the MongoDb users table manually.
184 |
185 | You can open the local website at [dev.puppy.pclub.in](dev.puppy.pclub.in)
186 | The backend will be listening on the printed port number.
187 |
188 | ### Contributors
189 | The following people have contributed to this project in various capacities:
190 | * [Saksham Sharma](https://github.com/sakshamsharma)
191 | * [Yash Srivastav](https://github.com/yashsriv)
192 | * [Kunal Kapila](https://github.com/kunalapila)
193 | * [Vinayak Tantia](https://github.com/vtantia)
194 | * [Milind Luthra](https://github.com/milindl)
195 |
--------------------------------------------------------------------------------
/frontend/src/app/components/results/results.component.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | Hello, {{ user.name }}
8 |
9 |
10 |
11 |
12 | Hi {{ user.name }}
13 |
14 |
15 | power_settings_new
16 | Logout
17 |
18 |
19 | About the platform
20 | Source Code
21 | pclub.in
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Your Matches:
35 |
36 |
39 | 0"
40 | fxLayout="column" fxLayoutAlign="center center">
41 |
Sorry, even though you received a lot of love, you didn't match with anyone.
42 |
43 |
45 |
Sorry, you didn't match with anyone.
46 |
You waited, but love did not come.
47 |
48 |
49 |
50 |
Hearts from people that the OA identifies as male:
51 |
52 |
53 |
54 |
55 |
56 |
Hearts from people that the OA identifies as female:
57 |
58 |
59 |
60 |
61 |
63 |
Sorry, you haven't gotten any hearts.
64 |
True love comes to those who wait.
65 |
66 |
67 |
68 |
69 | Stats
70 |
71 |
72 |
73 |
Registrations
74 |
80 |
81 |
82 |
83 |
Female Registrations
84 |
90 |
91 |
92 |
93 |
Male Registrations
94 |
100 |
101 |
102 |
103 |
Hearts Transferred
104 |
110 |
111 |
112 |
113 |
Female Hearts
114 |
120 |
121 |
122 |
123 |
Male Hearts
124 |
130 |
131 |
132 |
133 |
134 | Note: Males and Females refer to people identified as Male or Female by OA
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | Loading
144 |
145 |
--------------------------------------------------------------------------------
/frontend/src/app/services/main.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { HttpClient } from '@angular/common/http';
3 |
4 | import { BehaviorSubject } from 'rxjs/BehaviorSubject';
5 | import { Observable } from 'rxjs/Observable';
6 | import 'rxjs/add/observable/throw';
7 | import { of } from 'rxjs/observable/of';
8 | import 'rxjs/add/operator/toPromise';
9 | import { catchError, map, switchMap, tap } from 'rxjs/operators';
10 |
11 | import { Crypto } from '../crypto';
12 |
13 | export interface Stats {
14 | otherfemaleHearts: number;
15 | y18femaleHearts: number;
16 | y17femaleHearts: number;
17 | y16femaleHearts: number;
18 | y15femaleHearts: number;
19 | othermaleHearts: number;
20 | y18maleHearts: number;
21 | y17maleHearts: number;
22 | y16maleHearts: number;
23 | y15maleHearts: number;
24 | otherfemales: number;
25 | y18females: number;
26 | y17females: number;
27 | y16females: number;
28 | y15females: number;
29 | othermales: number;
30 | y18males: number;
31 | y17males: number;
32 | y16males: number;
33 | y15males: number;
34 | }
35 |
36 | interface LoginData {
37 | _id: string;
38 | name: string;
39 | email: string;
40 | gender: string;
41 | privKey: sjcl.SjclCipherEncrypted;
42 | pubKey: string;
43 | data: sjcl.SjclCipherEncrypted;
44 | submitted: boolean;
45 | matches: string;
46 | }
47 |
48 | interface MatchInfo {
49 | _id: string;
50 | matches: string;
51 | }
52 |
53 | interface Person {
54 | _id: string;
55 | name: string;
56 | email: string;
57 | }
58 |
59 | interface Heart {
60 | v: string;
61 | data: string;
62 | }
63 |
64 | interface Declare {
65 | _id: string;
66 | t0: string;
67 | t1: string;
68 | t2: string;
69 | t3: string;
70 | }
71 |
72 | interface ReceivedHeart {
73 | v: sjcl.SjclCipherEncrypted;
74 | genderOfSender: string;
75 | }
76 |
77 | interface User {
78 | _id: string;
79 | name: string;
80 | email: string;
81 | gender: string;
82 | submitted: boolean;
83 | crypto: Crypto;
84 | data: {
85 | choices: Person[],
86 | hearts: Heart[],
87 | lastCheck: number,
88 | received: ReceivedHeart[],
89 | }
90 | }
91 |
92 | @Injectable()
93 | export class MainService {
94 |
95 | user$: BehaviorSubject = new BehaviorSubject(null);
96 | matches$: BehaviorSubject = new BehaviorSubject(null);
97 | loggedIn = false;
98 |
99 | pubKeys: Map = new Map();
100 | people: Person[];
101 |
102 | constructor(private http: HttpClient) {
103 | this.http.get('/api/list/all').subscribe((res) => {
104 | this.people = res;
105 | });
106 | }
107 |
108 | private getInfo(username:string, password: string) {
109 | const info = this.http.get('/api/users/data/info')
110 | .pipe(
111 | map(x => {
112 | const crypto = new Crypto(password);
113 | crypto.deserializePriv(crypto.decryptSym(x.privKey));
114 | crypto.deserializePub(x.pubKey);
115 | const data = Crypto.toJson(crypto.decryptSym(x.data));
116 | return {
117 | _id: x._id,
118 | name: x.name,
119 | email: x.email,
120 | gender: x.gender,
121 | submitted: x.submitted,
122 | crypto,
123 | data: {
124 | choices: data.choices || [],
125 | hearts: data.hearts || [],
126 | lastCheck: data.lastCheck || 0,
127 | received: data.received || [],
128 | }
129 | };
130 | }),
131 | tap((user) => this.user$.next(user)),
132 | tap(() => this.loggedIn = true)
133 | );
134 | return info;
135 | }
136 |
137 | private declareChoices(pubkeys: Map) {
138 | const user = this.user$.value;
139 | let declarePayload = {'_id': user._id};
140 | let declarevalues = [];
141 | let heartvalues = [];
142 | let cnt = 0;
143 | for (let p of user.data.choices) {
144 | if (!pubkeys.has(p._id)) {
145 | console.log('Nk for ' + p._id);
146 | cnt = cnt + 1;
147 | continue;
148 | }
149 | const pubk = pubkeys.get(p._id);
150 |
151 | // Now calculate the heart values
152 | const cry = new Crypto();
153 | cry.deserializePub(pubk);
154 | heartvalues.push({
155 | 'v': cry.encryptAsym(Crypto.getRand(1)),
156 | 'data': cnt.toString(),
157 | 'genderOfSender': user.gender,
158 | });
159 |
160 | const pairId = (user._id > p._id ? (user._id + '-' + p._id) : (p._id + '-' + user._id));
161 | declarevalues.push(Crypto.hash(pairId + '-' + user.crypto.diffieHellman(pubk)));
162 | cnt = cnt + 1;
163 | }
164 |
165 | // Trim down choices to 4
166 | let count = Math.min(4, cnt);
167 | for (let i = 0; i < count; i++) {
168 | declarePayload['t' + i] = declarevalues[i];
169 | }
170 | for (let i = count; i < 4; i++) {
171 | declarePayload['t' + i] = '';
172 | }
173 |
174 | return this.http.post('/api/users/data/submit/' + user._id, {
175 | hearts: heartvalues,
176 | tokens: declarePayload
177 | }).pipe (
178 | tap(() => console.log('Saved hearts: ' + heartvalues.length + ' and declare values: ' + count)),
179 | );
180 |
181 | }
182 |
183 | private getPubKeys() {
184 | return this.http.get('/api/list/pubkey')
185 | .pipe (
186 | map(items => {
187 | const pubkeys: Map = new Map();
188 | for (let i in items) {
189 | pubkeys.set(items[i]['_id'], items[i]['pubKey']);
190 | }
191 | return pubkeys;
192 | })
193 | );
194 | }
195 |
196 | public getHearts(user: User) {
197 | return this.http.get<{ time: number, votes: ReceivedHeart[] }>('/api/hearts/get/' + (+user.data.lastCheck) + '/' + user._id)
198 | .pipe(
199 | map((hearts) => {
200 | const nuser = {
201 | ...user
202 | };
203 | nuser.data.lastCheck = hearts.time;
204 | const userhearts = nuser.data.received;
205 | for(let vote of hearts.votes) {
206 | const attempt = user.crypto.decryptAsym(vote.v);
207 | if (!attempt) {
208 | continue;
209 | }
210 | userhearts.push(vote);
211 | }
212 | nuser.data.received = userhearts;
213 | return nuser;
214 | }),
215 | tap((nuser) => this.user$.next(nuser)),
216 | tap(() => this.save())
217 | );
218 | }
219 |
220 | public submit() {
221 | return this.getPubKeys().pipe(
222 | switchMap((pub) => this.declareChoices(pub)),
223 | tap(() => this.save()),
224 | tap(() => {
225 | this.user$.next({
226 | ...this.user$.value,
227 | submitted: true,
228 | });
229 | })
230 | );
231 | }
232 |
233 | public login(username: string, _password: string) {
234 | const password = Crypto.hash(Crypto.hash(Crypto.hash(_password)));
235 | return this.http.post('/api/session/login', { username, password }).pipe(
236 | switchMap(() => this.getInfo(username, _password)),
237 | switchMap((user) => this.getHearts(user)),
238 | catchError((err) => {
239 | console.log(err);
240 | if (err.error instanceof Error) {
241 | // A client-side or network error occurred. Handle it accordingly.
242 | } else {
243 | // The backend returned an unsuccessful response code.
244 | // The response body may contain clues as to what went wrong,
245 | if (err.status === 202) {
246 | return of('');
247 | } else if (err.status === 403) {
248 | return Observable.throw('It seems you entered a wrong password');
249 | } else if (err.status === 404) {
250 | return Observable.throw('Your roll number is wrong. Please use your IITK roll no.');
251 | }
252 | }
253 | return Observable.throw('An Error Occurred!! Please try again.');
254 | })
255 | );
256 | }
257 |
258 | public signup(body: any) {
259 | return this.http.post('/api/users/login/first', body).pipe(
260 | catchError((err) => Observable.throw('Wrong Authentication Code!! Try Again'))
261 | );
262 | }
263 |
264 | public mail(roll: string): Observable {
265 | return this.http.get('/api/users/mail/' + roll).pipe(
266 | map(() => 'Mail sent to your @iitk ID !'),
267 | catchError((err) => {
268 | if (err.error instanceof Error) {
269 | // A client-side or network error occurred. Handle it accordingly.
270 | } else {
271 | // The backend returned an unsuccessful response code.
272 | // The response body may contain clues as to what went wrong,
273 | if (err.status === 202) {
274 | return of('Mail sent to your @iitk ID !');
275 | } else if (err.status === 404) {
276 | return of('Your information was not found in our database. Please send us a mail at pclubiitk@gmail.com');
277 | } else if (err.status === 400) {
278 | return of('You have already registered');
279 | }
280 | }
281 | return of('There was an error. Let us know at pclubiitk@gmail.com');
282 | })
283 | );
284 | }
285 |
286 | public matches(): Observable {
287 | const user = this.user$.value;
288 | return this.http.get('/api/users/data/match/' + user._id).pipe(
289 | tap(x => this.matches$.next(x)),
290 | catchError((err) => {
291 | console.error(err);
292 | return Observable.throw('There was an error. Please try again.');
293 | })
294 | );
295 | }
296 |
297 | public stats(): Observable {
298 | return this.http.get('/api/stats').pipe(
299 | catchError((err) => {
300 | console.error(err);
301 | return Observable.throw('There was an error. Please try again.');
302 | }),
303 | );
304 | }
305 |
306 | save(): Promise {
307 | const user = this.user$.value;
308 | const data = user.data;
309 | console.log(data);
310 | const encData = user.crypto.encryptSym(Crypto.fromJson(data));
311 | return this.http.post('/api/users/data/update/' + user._id, {
312 | data: encData,
313 | }).toPromise();
314 | }
315 |
316 | }
317 |
--------------------------------------------------------------------------------
/controllers/user.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "time"
9 |
10 | "github.com/pclubiitk/puppy-love/db"
11 | "github.com/pclubiitk/puppy-love/models"
12 | "github.com/pclubiitk/puppy-love/utils"
13 |
14 | "github.com/gin-gonic/gin"
15 | "gopkg.in/mgo.v2/bson"
16 | )
17 |
18 | var Db db.PuppyDb
19 |
20 | func UserDelete(c *gin.Context) {
21 | id, err := SessionId(c)
22 | if err != nil || id != "admin" {
23 | c.AbortWithStatus(http.StatusForbidden)
24 | return
25 | }
26 |
27 | if err := Db.GetCollection("user").DropCollection(); err != nil {
28 | c.String(http.StatusInternalServerError,
29 | "Could not delete collection")
30 | return
31 | }
32 |
33 | c.String(http.StatusOK, "Deleted user table")
34 | }
35 |
36 | func UserNew(c *gin.Context) {
37 | id, err := SessionId(c)
38 | if err != nil || id != "admin" {
39 | c.AbortWithStatus(http.StatusForbidden)
40 | log.Print("Unauthorized creation attempt by: " + id)
41 | log.Print(err)
42 | return
43 | }
44 |
45 | info := new(models.TypeUserNew)
46 | if err := c.BindJSON(info); err != nil {
47 | c.AbortWithStatus(http.StatusBadRequest)
48 | return
49 | }
50 |
51 | user := models.NewUser(info)
52 |
53 | if err := Db.GetCollection("user").Insert(&user); err != nil {
54 | c.AbortWithStatus(http.StatusInternalServerError)
55 | log.Print(err)
56 | return
57 | }
58 |
59 | c.JSON(http.StatusAccepted, "Information set up")
60 | }
61 |
62 | // User's first login
63 | // ------------------
64 | func UserFirst(c *gin.Context) {
65 | info := new(models.TypeUserFirst)
66 | if err := c.BindJSON(info); err != nil {
67 | c.AbortWithStatus(http.StatusBadRequest)
68 | return
69 | }
70 |
71 | user := models.User{}
72 |
73 | // Fetch user
74 | if err := Db.GetById("user", info.Id).One(&user); err != nil {
75 | c.AbortWithStatus(http.StatusNotFound)
76 | log.Print(err)
77 | return
78 | }
79 |
80 | // If auth code did not match
81 | if user.AuthC != info.AuthCode || user.AuthC == "" {
82 | c.AbortWithStatus(http.StatusForbidden)
83 | return
84 | }
85 |
86 | // Edit information
87 | if _, err := Db.GetById("user", info.Id).
88 | Apply(user.FirstLogin(info), &user); err != nil {
89 |
90 | c.AbortWithStatus(http.StatusInternalServerError)
91 | log.Print(err)
92 | return
93 | }
94 |
95 | // Remove user's auth token
96 | if _, err := Db.GetById("user", info.Id).
97 | Apply(user.SetField("autoCode", ""), &user); err != nil {
98 |
99 | c.AbortWithStatus(http.StatusInternalServerError)
100 | log.Print(err)
101 | return
102 | }
103 |
104 | c.JSON(http.StatusAccepted, "Information set up")
105 | }
106 |
107 | // User asking for email
108 | // ---------------------
109 | func UserMail(c *gin.Context) {
110 | id := c.Param("id")
111 |
112 | type mailData struct {
113 | Email string `json:"email" bson:"email"`
114 | AuthC string `json:"authCode" bson:"authCode"`
115 | }
116 |
117 | u := mailData{}
118 |
119 | if err := Db.GetById("user", id).One(&u); err != nil {
120 | c.AbortWithStatus(http.StatusNotFound)
121 | log.Print(err)
122 | return
123 | }
124 |
125 | if u.AuthC == "" {
126 | c.String(http.StatusBadRequest, "You have already signed up")
127 | return
128 | }
129 |
130 | // Queue this request in service
131 | err := utils.SignupRequest(id)
132 | if err != nil {
133 | c.String(http.StatusInternalServerError, "Something went wrong")
134 | }
135 |
136 | c.JSON(http.StatusAccepted,
137 | fmt.Sprintf("Mail will be sent to %s", u.Email))
138 | }
139 |
140 | func MatchGet(c *gin.Context) {
141 | id, err := SessionId(c)
142 | if err != nil || c.Param("you") != id {
143 | c.AbortWithStatus(http.StatusForbidden)
144 | log.Println("Failed on match get: " + id)
145 | log.Println(err)
146 | return
147 | }
148 |
149 | type typeUserGet struct {
150 | ID string `json:"_id" bson:"_id"`
151 | Matches string `json:"matches" bson:"matches"`
152 | }
153 |
154 | user := new(typeUserGet)
155 |
156 | // Fetch user
157 | if err := Db.GetById("user", id).One(user); err != nil {
158 | c.AbortWithStatus(http.StatusNotFound)
159 | log.Print(err)
160 | return
161 | }
162 |
163 | c.JSON(http.StatusOK, (*user))
164 | }
165 |
166 | // Get user's information
167 | // ----------------------
168 | type typeUserGet struct {
169 | Id string `json:"_id" bson:"_id"`
170 | Name string `json:"name" bson:"name"`
171 | Gender string `json:"gender" bson:"gender"`
172 | Image string `json:"image" bson:"image"`
173 | PubK string `json:"pubKey" bson:"pubKey"`
174 | }
175 |
176 | func UserGet(c *gin.Context) {
177 | id := c.Param("id")
178 |
179 | user := models.User{}
180 |
181 | // Fetch user
182 | if err := Db.GetById("user", id).One(&user); err != nil {
183 | c.AbortWithStatus(http.StatusNotFound)
184 | log.Print(err)
185 | return
186 | }
187 |
188 | resp := typeUserGet{
189 | Id: user.Id,
190 | Name: user.Name,
191 | Gender: user.Gender,
192 | Image: user.Image,
193 | PubK: user.PubK,
194 | }
195 |
196 | c.JSON(http.StatusAccepted, resp)
197 | }
198 |
199 | // @AUTH Get user's private information on login
200 | // ---------------------------------------
201 |
202 | type typeUserLoginGet struct {
203 | Id string `json:"_id" bson:"_id"`
204 | Name string `json:"name" bson:"name"`
205 | Gender string `json:"gender" bson:"gender"`
206 | Image string `json:"image" bson:"image"`
207 | PrivK string `json:"privKey" bson:"privKey"`
208 | PubK string `json:"pubKey" bson:"pubKey"`
209 | Data string `json:"data" bson:"data"`
210 | Submit bool `json:"submitted" bson:"submitted"`
211 | Matches string `json:"matches" bson:"matches"`
212 | Email string `json:"email" bson:"email"`
213 | }
214 |
215 | func UserLoginGet(c *gin.Context) {
216 | id, err := SessionId(c)
217 | if err != nil {
218 | c.AbortWithStatus(http.StatusForbidden)
219 | log.Println("Failed on login info: " + id)
220 | log.Println(err)
221 | return
222 | }
223 |
224 | user := models.User{}
225 |
226 | // Fetch user
227 | if err := Db.GetById("user", id).One(&user); err != nil {
228 | c.AbortWithStatus(http.StatusNotFound)
229 | log.Print(err)
230 | return
231 | }
232 |
233 | resp := typeUserLoginGet{
234 | Id: user.Id,
235 | Name: user.Name,
236 | Email: user.Email,
237 | Gender: user.Gender,
238 | Image: user.Image,
239 | PrivK: user.PrivK,
240 | PubK: user.PubK,
241 | Data: user.Data,
242 | Submit: user.Submit,
243 | Matches: user.Matches,
244 | }
245 |
246 | c.JSON(http.StatusAccepted, resp)
247 | }
248 |
249 | // After user submits all choices
250 | // ------------------------------
251 |
252 | func UserSubmitTrue(c *gin.Context) {
253 | id, err := SessionId(c)
254 | if err != nil || id != c.Param("you") {
255 | c.AbortWithStatus(http.StatusForbidden)
256 | return
257 | }
258 |
259 | user := models.User{}
260 | if err := Db.GetById("user", id).One(&user); err != nil {
261 | c.JSON(http.StatusNotFound, "Invalid user")
262 | log.Print(err)
263 | return
264 | }
265 |
266 | heartsAndChoices := new(models.HeartsAndChoices)
267 | if err := c.BindJSON(heartsAndChoices); err != nil {
268 | c.String(http.StatusBadRequest, "Invalid JSON")
269 | log.Print(err)
270 | return
271 | }
272 |
273 | // First, send the hearts using sendHearts
274 | if err = sendHearts(user, heartsAndChoices.Hearts); err != nil {
275 | c.JSON(http.StatusBadRequest, "Failed, probably the request is invalid")
276 | log.Print(err)
277 | return
278 | }
279 |
280 | // Then, declare the choices
281 | if err = declareStep(user, heartsAndChoices.Tokens); err != nil {
282 | c.JSON(http.StatusBadRequest, "Failed, probably the request is invalid")
283 | log.Print(err)
284 | return
285 | }
286 |
287 | if _, err := Db.GetById("user", id).
288 | Apply(user.SetField("submitted", true), &user); err != nil {
289 |
290 | c.AbortWithStatus(http.StatusInternalServerError)
291 | log.Print(err)
292 | return
293 | }
294 |
295 | }
296 |
297 | func declareStep(user models.User, info models.Declare) error {
298 |
299 | if info.Id != user.Id {
300 | return errors.New("Invalid session/userId")
301 | }
302 |
303 | // TODO: fix db name to not be a constant
304 | if _, err := Db.GetCollection("declare").UpsertId(user.Id, bson.M{
305 | "t0": info.Token0,
306 | "t1": info.Token1,
307 | "t2": info.Token2,
308 | "t3": info.Token3,
309 | }); err != nil {
310 | return err
311 | }
312 | return nil
313 | }
314 |
315 | func difference(oldVotes []models.Heart,
316 | newVotes []models.GotHeart) []models.GotHeart {
317 |
318 | diff := []models.GotHeart{}
319 | m := map[string]int{}
320 | for _, s1val := range oldVotes {
321 | m[s1val.Data] = 1
322 | }
323 |
324 | for _, s2val := range newVotes {
325 | if m[s2val.Data] != 1 {
326 | diff = append(diff, s2val)
327 | }
328 | }
329 |
330 | return diff
331 | }
332 |
333 | // Serve when a Heart is to be saved
334 | func sendHearts(user models.User, info []models.GotHeart) error {
335 | // Check that user isn't voting more than 4
336 | // ========================================
337 |
338 | userVotes := new([]models.Heart)
339 | if err := Db.GetCollection("heart").
340 | Find(bson.M{"roll": user.Id}).
341 | All(userVotes); err != nil {
342 | return err
343 | }
344 |
345 | diffHearts := difference(*userVotes, info)
346 |
347 | log.Print("Earlier count: ", len(*userVotes))
348 | log.Print("Sent new: ", len(diffHearts))
349 |
350 | if len(diffHearts)+len(*userVotes) > 4 {
351 | return errors.New("More than allowed votes")
352 | }
353 |
354 | ctime := uint64(time.Now().UnixNano() / 1000000)
355 |
356 | newHearts := []models.Heart{}
357 | for _, heart := range diffHearts {
358 | newHearts = append(newHearts,
359 | models.Heart{
360 | Id: user.Id,
361 | Gender: heart.GenderOfSender,
362 | Time: ctime,
363 | Value: heart.Value,
364 | Data: heart.Data,
365 | })
366 | }
367 |
368 | bulk := Db.GetCollection("heart").Bulk()
369 | for _, heart := range newHearts {
370 | bulk.Insert(heart)
371 | }
372 |
373 | _, err := bulk.Run()
374 |
375 | if err != nil {
376 | return err
377 | }
378 | return nil
379 | }
380 |
381 | // @AUTH Update user data
382 | // ------------------------------
383 |
384 | func UserUpdateData(c *gin.Context) {
385 | id, err := SessionId(c)
386 | if err != nil || id != c.Param("you") {
387 | c.AbortWithStatus(http.StatusForbidden)
388 | return
389 | }
390 |
391 | type typeUserUpdateData struct {
392 | Data string `json:"data"`
393 | }
394 |
395 | info := new(typeUserUpdateData)
396 | if err := c.BindJSON(info); err != nil {
397 | c.AbortWithStatus(http.StatusBadRequest)
398 | return
399 | }
400 |
401 | user := models.User{}
402 |
403 | if _, err := Db.GetById("user", id).
404 | Apply(user.SetField("data", info.Data), &user); err != nil {
405 |
406 | c.AbortWithStatus(http.StatusInternalServerError)
407 | log.Print(err)
408 | return
409 | }
410 |
411 | c.JSON(http.StatusAccepted, "Saved successfully")
412 | }
413 |
414 | // @AUTH Update user image
415 | // ------------------------------
416 |
417 | func UserUpdateImage(c *gin.Context) {
418 | id, err := SessionId(c)
419 | if err != nil || id != c.Param("you") {
420 | c.AbortWithStatus(http.StatusForbidden)
421 | return
422 | }
423 |
424 | type imgstruct struct {
425 | Image string `json:"img" bson:"img"`
426 | }
427 |
428 | user := models.User{}
429 | info := new(imgstruct)
430 |
431 | if err := c.BindJSON(info); err != nil {
432 | c.AbortWithStatus(http.StatusBadRequest)
433 | return
434 | }
435 |
436 | if _, err := Db.GetById("user", id).
437 | Apply(user.SetField("image", info.Image), &user); err != nil {
438 |
439 | c.AbortWithStatus(http.StatusInternalServerError)
440 | log.Print(err)
441 | return
442 | }
443 |
444 | c.JSON(http.StatusAccepted, "Saved successfully")
445 | }
446 |
447 | // @AUTH Update user passsave
448 | // ------------------------------
449 | func UserSavePass(c *gin.Context) {
450 | id, err := SessionId(c)
451 | if err != nil || id != c.Param("you") {
452 | c.AbortWithStatus(http.StatusForbidden)
453 | return
454 | }
455 |
456 | type imgstruct struct {
457 | Pass string `json:"pass" bson:"pass"`
458 | }
459 |
460 | user := models.User{}
461 | info := new(imgstruct)
462 |
463 | if err := c.BindJSON(info); err != nil {
464 | c.AbortWithStatus(http.StatusBadRequest)
465 | return
466 | }
467 |
468 | if _, err := Db.GetById("user", id).
469 | Apply(user.SetField("savepass", info.Pass), &user); err != nil {
470 |
471 | c.AbortWithStatus(http.StatusInternalServerError)
472 | log.Print(err)
473 | return
474 | }
475 |
476 | c.JSON(http.StatusAccepted, "Saved successfully")
477 | }
478 |
--------------------------------------------------------------------------------
/report/puppylove.tex:
--------------------------------------------------------------------------------
1 | %
2 | % LaTeX template for prepartion of submissions to SIGTBD'16
3 | %
4 | % Requires temporary version of sigplanconf style file provided on
5 | % SIGTBD'16 web site.
6 | %
7 | \documentclass[sigtbd]{sigtbd-style}
8 | % \documentclass[SIGTBD-cameraready]{sigplanconf-SIGTBD16}
9 | %
10 | % the following standard packages may be helpful, but are not required
11 | %
12 | \usepackage{courier} % standard fixed width font
13 | \usepackage[scaled]{helvet} % see www.ctan.org/get/macros/latex/required/psnfss/psnfss2e.pdf
14 | \usepackage{url} % format URLs
15 | \usepackage{listings} % format code
16 | \usepackage{epigraph}
17 | \usepackage{enumitem} % adjust spacing in enums
18 | \usepackage[colorlinks=true,allcolors=blue,breaklinks,draft=false]{hyperref} % hyperlinks, including DOIs and URLs in bibliography
19 | % known bug: http://tex.stackexchange.com/questions/1522/pdfendlink-ended-up-in-different-nesting-level-than-pdfstartlink
20 | \newcommand{\doi}[1]{doi:~\href{http://dx.doi.org/#1}{\Hurl{#1}}} % print a hyperlinked DOI
21 |
22 | % Comments
23 | \newcommand{\va}[1]{\textcolor{red}{{\em VA:} #1}}
24 | \newcommand{\sss}[1]{\textcolor{red}{{\em SS:} #1}}
25 | \newcommand{\mv}[1]{\textcolor{red}{{\em MV:} #1}}
26 |
27 | \newcommand{\name}{\textit{Puppy Love}}
28 |
29 | \begin{document}
30 |
31 | \title{\name{}: Cryptographically secure anonymous couple matching}
32 |
33 | %
34 | % any author declaration will be ignored when using 'SIGTBD' option (for double blind review)
35 | %
36 |
37 | \authorinfo{Saksham Sharma}
38 | {\makebox{Computer Science and Technology} \\
39 | \makebox{Indian Institute of Technology Kanpur} \\
40 | {sakshams@cse.iitk.ac.in}}
41 |
42 | \maketitle
43 | \begin{abstract}
44 | Anonymous and secure dating algorithms are not only hard to define,
45 | but apparently tough to realize as well. Tinder et all have
46 | succeeded in making this a mainstream application, and yet, there is
47 | scope left in improving the anonymity of such platforms to provide
48 | a guarantee to the users that their choices shall not be made
49 | public, nor known to the administrators.
50 |
51 | We designed an algorithm for this problem which provides strong
52 | guarantees about privacy of users, and implemented it inside our
53 | campus, catering to almost 2000 users. The algorithm is designed to
54 | be easily scalable, and was a success in the university setting.
55 | \end{abstract}
56 |
57 | \section{Introduction}
58 |
59 | The queerly named \title{} platform has been running in IIT Kanpur
60 | since 2014, meant to help shy nerds meet their crush. The platform
61 | opens one week prior to the Valentine's Day every year, and lets people
62 | choose up to 4 of their crushes. At the stroke of the midnight hour on
63 | 14th February, people who happen to like each other are informed about
64 | the same. If your \textit{love} was unrequited (the other person
65 | didn't like you), you will not get to know. More importantly, if you
66 | did not like the other person, you would not know if that person liked
67 | you or not. In addition, people can see the count of people who have
68 | selected their names without finding out who they are.
69 |
70 | Since a campus is often a close knit circle, users are apprehensive
71 | about revealing their crushes to others, including the administrators
72 | (who are students themselves), lest it leak out. That makes it
73 | necessary to design a technique to ensure that users are guaranteed
74 | that obtaining their choices is either impossible or computationally
75 | infeasible for anyone but them.
76 |
77 | We developed a new version of the platform which provides strong
78 | privacy guarantees without compromising on performance or features.
79 |
80 | \section{Requirements from the algorithm}
81 | The following discussion refers to $Alice$ and $Bob$. All
82 | arguments apply to each of them.\\
83 | The security goals are the following:
84 | \begin{enumerate}
85 | \item $Alice$ should not find out the choice of person $Bob$ if $Alice$
86 | did not like $Bob$.
87 | \item It should not be possible for $Alice$ to cheat and obtain
88 | whether $Alice$ matched with the $Bob$, without this
89 | information being made available for $Bob$. In other words, the
90 | algorithm should be fair, and ideally symmetric.
91 | \item The administrator should not be able to obtain the choices of
92 | a person, or brute force the data in any way to deduce the likes
93 | of a person.
94 | \item The administrator should not find out which people matched, but
95 | knowing the count of matches is reasonable.
96 | \item The user should be able to verify that the frontend is not
97 | sending any such information which could reveal their
98 | choices. Thus, the privacy guarantees should hold whatever code is
99 | running on the backend.
100 | \item The counting technique should function independently without
101 | revealing the identities of the users.
102 | \item Security features should be practical to implement in a web
103 | browser and should be completely transparent to the user.
104 | \item It can be assumed that the server does not collude with
105 | another involved party. Thus, the server is assumed to be
106 | honest-but-curious and will not disrupt the service intentionally.
107 | \end{enumerate}
108 | Some other practical requirements:
109 | \begin{enumerate}
110 | \item The algorithm should have no false positives or false negatives.
111 | \item It should ideally involve only $O(1)$ computation by each
112 | involved party.
113 | \item There should ideally be no to-and-fro communication between the
114 | involved parties.
115 | \end{enumerate}
116 |
117 | \section{Related work}
118 | A similar problem was proposed and tackled in \cite{Senpai:1} where
119 | the proposed various algorithms, including one which does present a
120 | secure protocol. The drawback of such an algorithm is that it requires
121 | all involved parties to take part in $O(n)$ computation, where $n$ is
122 | the number of other parties.
123 |
124 | Our algorithm is completely different from this work, but it is
125 | interesting to look at the mentioned paper nonetheless.
126 |
127 | \bibliographystyle{abbrvnat}
128 |
129 | \section{Initial approaches}
130 | We went through a lot of approaches for this problem, before finally
131 | arriving at the presented approach, which, although extremely simple,
132 | is not obvious at first.
133 |
134 | We initially modelled the problem as computing the AND of $O(n^2)$ bits,
135 | disregarding the requirement of $O(1)$ computations per person.
136 |
137 | \subsection{Homomorphic computation}
138 |
139 | Our initial attempts at homomorphic were foiled by the fact that
140 | completely homomorphic systems are impractical.
141 |
142 | In contrast, partially homomorphic functions, although practical, do
143 | not provide any function which can be used for this purpose. The
144 | available computations are XOR, multiply, addition over 1 bit et
145 | all. Composing any two of these functions provides enough information
146 | to both parties to violate privacy concerns.
147 |
148 | \subsection{Yao's two party computation}
149 | Yao presented a secure protocol to compute any arbitrary function
150 | (which can be run using logic gates) across two parties in a shared
151 | manner. The protocol was not originally intended to be fair though,
152 | and one party could cheat in some respect.
153 |
154 | We describe the protocol here:
155 | \begin{itemize}
156 | \item Alice party creates a truth table for an AND gate, with columns
157 | A (her choice), B (Bob's choice) and C (result).
158 | \item She selects 6 random values $a_0, a_1, b_0, b_1, c_0, c_1$. She
159 | then shuffles the table, and encrypts $((A = i) \& (B = j)) = Ck$
160 | with the value $a_i-b_j$. She then shares this table with Bob.
161 | \item If Alice's boolean choice is $choice$, she sends $a_{choice}$ to
162 | Bob as well.
163 | \item Bob obtains $b_{bob's choice}$ from Alice using Oblivious
164 | transfer, such that Alice will not know which bit value was obtained
165 | by Bob.
166 | \item Bob tries decrypting the table with the key $a_{alice}-b_{bob}$
167 | and finds out what value he obtained. He can then look up whether
168 | they matched or not.
169 | \end{itemize}
170 |
171 | We attempted to make the protocol fair by using the central server,
172 | who would then match both parties and declare their result to them.
173 |
174 | A clever hack around this was found on noticing the asymmetry in the
175 | algorithm. Alice could forge the truth table to return a $mismatch$ if
176 | Bob likes Alice, and a $match$ otherwise. A false positive can be
177 | borne easily, but if Bob did like Alice, he would not know that Alice
178 | has cheated and has found out about his liking. This is certainly
179 | unacceptable.
180 |
181 | \subsection{Public key encrypted messages}
182 | This involves the following step:
183 | \begin{itemize}
184 | \item All parties get to send 4 values to the server.
185 | \item If Alice likes Bob, she will send a message (string containing
186 | their roll numbers) encrypted with Bob's
187 | public key to the server.
188 | \item Bob will encrypt the same message with his own public key and
189 | store it on the server.
190 | \item The person to use the other's public key is chosen using
191 | lexicographic comparison.
192 | \item The server will detect duplicates and declare matches.
193 | \end{itemize}
194 |
195 | This is a very simple algorithm, but fails on a computationally
196 | feasible brute force attack. The server can easily try out all
197 | possible messages which Alice could have sent, and stop when it
198 | produces a value which is the same as the one sent by Alice.
199 |
200 | \subsection{An O(n) algorithm using duplicates}
201 | We observed that instead of trying to compute an AND, it may be
202 | interesting to instead use the duplicate detection in another way.
203 |
204 | An algorithm for this is quite simple:
205 | \begin{itemize}
206 | \item Both parties establish an encrypted channel using their public
207 | keys.
208 | \item They both exchange a random token over this channel. Let the
209 | tokens be $A, B$.
210 | \item Both of them declare a value $hash(A+B)$ to the server, who
211 | verifies that the values are the same.
212 | \item Each party gets to send 4 new values to the server.
213 | \item If Alice likes Bob, she will send the pre-decided $A$ to the
214 | server. Otherwise she will send a random value.
215 | \item Server computes $hash(A_{received}-B_{received})$ and matches it
216 | with the values received earlier. If they are the same, it declares
217 | a match.
218 | \end{itemize}
219 |
220 | Although this algorithm has no obvious security concerns, it involves
221 | a lot of computation, infact $O(n)$ computation per person. Our tests
222 | showed a huge performance bottleneck on modern browsers while using
223 | Stanford's $sjcl$ library for crypto-protocol implementations.
224 |
225 | The platform originally launched with this algorithm, but the
226 | performance issues on scaling to $O(1000)$ people made it practically
227 | impossible to run.
228 |
229 | \section{The Puppy Love algorithm}
230 | The final algorithm was inspired by the previous $O(n)$ algorithm,
231 | wherein it was identified that it is essential to come up with a
232 | message which only the involved parties can create, and no one
233 | else. This is the only step which was causing a $O(n)$ bottleneck.
234 |
235 | Apart from that, some other requirements (counting the number of
236 | people who like you, stopping the server from knowing the matched
237 | people) required some additions to this algorithm which are described
238 | later.
239 |
240 | \subsection{Matching}
241 | \begin{itemize}
242 | \item There is a known generator field $g$.
243 | \item Let the public key of Alice be $g^a$ and her private key be
244 | $a$. The same is true for Bob's $g^b$ and $b$.
245 | \item The value $g^{ab}$ cannot be computed by anyone other than Alice
246 | and Bob, since it is the Discrete Logarithm problem, which is known
247 | to not lie in PTime.
248 | \item Both parties get to send 4 values to the server.
249 | \item If Alice likes Bob, she will send $g^{ab}$ to
250 | the server. The same applies to Bob.
251 | \item The server declares a match if it detects a duplicate value.
252 | \end{itemize}
253 |
254 | Some remarks:
255 | \begin{enumerate}
256 | \item The algorithm itself is quite simple, and does not present any
257 | apparent privacy concerns. The only issue (to be resolved later) is
258 | that the server will know which two people have matched.
259 | \item It is extremely fast to implement, and running times are ideal
260 | for browsers, since it involves only 4 maximum mathematical
261 | computations.
262 | \item Values received by the server appear completely random to it,
263 | and cannot be brute forced.
264 | \item The value of $g^{ab}$ is very easily obtained if the
265 | Public-Private keypairs are ElGamal, as was in our case. This value
266 | is simply the Diffie Hellman value computed in Elliptic Curve
267 | Cryptography, and Stanford's $sjcl$ library provides a direct
268 | function for this computation.
269 | \end{enumerate}
270 |
271 | \subsection{Implementing the counting of people who like you}
272 | We opted for a very simple solution to this, albeit it has a minor scope
273 | for inconsistencies.
274 |
275 | When Alice sends her preferences, her client also encrypts a
276 | random string with the public key of Bob (if Bob is among her
277 | likes). This value is stored on the server in a list ordered by
278 | timestamps. Other clients retrieve all new entries in this list
279 | everytime they log back in, and try decrypting all received
280 | values. The client can detect that some of the messages were encrypted
281 | with the user's public key, but cannot detect the sender of the
282 | message.
283 |
284 | \subsection{Ensuring that the server does not know the matches}
285 | This was not implemented during 2017's deployment of the platform, but
286 | should work fine regardless.
287 | \begin{itemize}
288 | \item Before sending the final chosen values to the server, the client
289 | obtains the blind signatures of the values it plans to send.
290 | \item The blind signatures, as well as the actual values which are
291 | being sent will not be known to the server.
292 | \item The client now switches IP (in our case, sends the packet
293 | through the NAT of the university, re-entering the university
294 | network using a reverse proxy, thus completely anonymizing the
295 | identity of the sender), and sends its 4 tokens (with
296 | signature) to the server without attaching its login cookie with
297 | them. The server can only verify that the received values are
298 | correctly signed.
299 | \item The server declares all the matched tokens in the end, and
300 | clients can check if their token is among this list of matched
301 | tokens.
302 | \end{itemize}
303 |
304 | \subsection{Miscellaneous security concerns}
305 | There are some other concerns which are discussed here:
306 | \begin{itemize}
307 | \item IP switching is unreliable. The user may not be expected to
308 | undertake this arduous task of accessing the platform behind a
309 | VPN/Tor for the step of sending the tokens. Thankfully, our
310 | university setup had a network which is known to be not in the
311 | control of the students, and thus the packets may use the NAT to
312 | ensure their identities are safe.
313 | \item It is possible for the server to display fake public keys for
314 | users. This surely is a concern, but can be easily verified. The
315 | server does not ask for cookies while serving public keys, and thus,
316 | it can be easily verified (by a concerned user) whether a different
317 | public key is being server to some people. If the university
318 | infrastructure allowed for reliable hosting of public keys, this
319 | task could be eased.
320 | \item The server may serve fake code for some users which reveals more
321 | information. But in such a case, the hashes of the code received and
322 | the code expected will not match, and a concerned user can verify
323 | the same, or run a local copy of the code (which she has manually
324 | verified to be safe).
325 | \end{itemize}
326 |
327 | \section{Deployment}
328 | The platform was well received, with more than 1800 students
329 | registering on the website, and as many as 45 matched couples. There
330 | were many people who had multiple matches, both among boys and girls.
331 |
332 | Although girls are outnumbered on the campus $1:10$, among the
333 | registrants, the ratio was close to $1:5$.
334 |
335 | The backend was implemented in Golang, using the $iris$ web
336 | framework. The frontend was written using Angular2 and TypeScript,
337 | communicating with the backend using REST API of the backend. The
338 | server resource usage was monitored and was observed to be very low
339 | even during peak times, due to Golang's lightweight web
340 | frameworks. MongoDB was used for data storage, and Redis was used to
341 | ensure persistent sessions.
342 |
343 | \begin{thebibliography}{}
344 | \softraggedright
345 | \bibitem[(2015)]{Senpai:1}
346 | Senpai: Solving the dating problem
347 | \newblock SIGTBD 2017.
348 |
349 | \end{thebibliography}
350 |
351 | \end{document}
352 |
353 | %%% Local Variables:
354 | %%% mode: latex
355 | %%% TeX-master: t
356 | %%% End:
357 |
--------------------------------------------------------------------------------