
now the server accepts more forms of data, like accelometer, gyroscope, orientation, and azimuth
@ae080f2cc4ed676021d2840c66ba634be62dec43
+++ README.md
... | ... | @@ -0,0 +1,1 @@ |
1 | +# Daegu-Jeju-DIP |
+++ action.py
... | ... | @@ -0,0 +1,296 @@ |
1 | + | |
2 | +from flask_restx import Resource, Namespace, fields | |
3 | +from flask import Flask, request | |
4 | +import os | |
5 | +from database.database import DB | |
6 | +import pandas as pd | |
7 | +import jwt | |
8 | +import datetime | |
9 | + | |
10 | + | |
11 | +paths = os.getcwd() | |
12 | + | |
13 | +Action = Namespace( | |
14 | + name="Action", | |
15 | + description="노드 분석을 위해 사용하는 api.", | |
16 | +) | |
17 | + | |
18 | +trip_log_model = Action.model('TripLog', { | |
19 | + 'trip_id': fields.String(required=True, description='The ID of the trip (64 characters)'), | |
20 | + 'trip_distance_m': fields.Float(required=True, description='Total distance traveled in meters'), | |
21 | + 'trip_time_s': fields.Float(required=True, description='Total time of the trip in seconds'), | |
22 | + 'abrupt_start_count': fields.Integer(required=True, description='Count of abrupt starts'), | |
23 | + 'abrupt_stop_count': fields.Integer(required=True, description='Count of abrupt stops'), | |
24 | + 'abrupt_acceleration_count': fields.Integer(required=True, description='Count of abrupt accelerations'), | |
25 | + 'abrupt_deceleration_count': fields.Integer(required=True, description='Count of abrupt decelerations'), | |
26 | + 'helmet_on': fields.Integer(required=True, description='Whether the helmet was worn during the trip, must be 0 or 1 with 1 is the helmet on.'), | |
27 | + 'final_score': fields.Float(required=True, description='The final safety score for the trip') | |
28 | +}) | |
29 | + | |
30 | +history_request_model = Action.model( | |
31 | + 'history_request', { | |
32 | + 'user_id' : fields.String(required=True, description = 'The user ID that you want to query history') | |
33 | + } | |
34 | +) | |
35 | + | |
36 | + | |
37 | +@Action.route('/gps_update') | |
38 | +class GPS_update(Resource): | |
39 | + @Action.doc(responses={200: 'Success'}) | |
40 | + @Action.doc(responses={401: 'Unauthorized'}) | |
41 | + @Action.doc(responses={500: 'Internal Error'}) | |
42 | + def post(self): | |
43 | + token = request.headers.get('Authorization') | |
44 | + if not token: | |
45 | + return {'result': 'fail', 'msg': '토큰이 없습니다.'} | |
46 | + try: | |
47 | + # Decode the token to verify it | |
48 | + decoded_token = jwt.decode(token, "secret", algorithms=['HS256']) | |
49 | + user_id = decoded_token['id'] | |
50 | + except jwt.ExpiredSignatureError: | |
51 | + return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401 | |
52 | + except jwt.InvalidTokenError: | |
53 | + return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401 | |
54 | + | |
55 | + db = DB() | |
56 | + | |
57 | + data = request.get_json() | |
58 | + if len(data["trip_id"]) !=64: | |
59 | + return {500 :"ERROR! INVALID TRIP_ID!"} | |
60 | + | |
61 | + if len(data["trip_log"]["timestamp"]) == 0: | |
62 | + return {500 :"ERROR! 'trip_log' is empty!"} | |
63 | + | |
64 | + time_stamp_len = len(data["trip_log"]["timestamp"]) | |
65 | + latitude_len = len(data["trip_log"]["latitude"]) | |
66 | + longitude_len = len(data["trip_log"]["longitude"]) | |
67 | + | |
68 | + if time_stamp_len != latitude_len or latitude_len != longitude_len: | |
69 | + return { | |
70 | + 500: f"ERROR! Mismatching length of data in trip_log! \n timestamp : {time_stamp_len} \n latitude : {latitude_len} \n longitude : {longitude_len}" | |
71 | + } | |
72 | + with pd.option_context('display.max_rows', None, 'display.max_columns', None): | |
73 | + print(data) | |
74 | + df = pd.DataFrame(data["trip_log"]) | |
75 | + df["user_id"] = data["user_id"] | |
76 | + df["trip_id"] = data["trip_id"] | |
77 | + | |
78 | + columns = df.columns | |
79 | + data_csv_block = df.to_csv(header=False, index=False) | |
80 | + print(f"-------------------------------------") | |
81 | + print(f"recieved trip_id : {df['trip_id'][0]}") | |
82 | + print(f"recieved time : {datetime.datetime.now()}") | |
83 | + # GPS 데이터베이스에 삽입 | |
84 | + db.insert_gps_data(data_csv_block, columns) | |
85 | + | |
86 | + acc_df = pd.DataFrame({ | |
87 | + 'trip_id': data["trip_id"], | |
88 | + 'user_id': data["user_id"], | |
89 | + 'accel_x': data["accel"]["x"], | |
90 | + 'accel_y': data["accel"]["y"], | |
91 | + 'accel_z': data["accel"]["z"], | |
92 | + 'timestamp': data["accel"]["timestamp"] | |
93 | + }) | |
94 | + accel_columns_ordered = ['trip_id', 'user_id', 'timestamp', 'accel_x', 'accel_y', 'accel_z'] | |
95 | + accel_csv_data = acc_df[accel_columns_ordered].to_csv(header=False, index=False) | |
96 | + db.insert_accel_data(accel_csv_data, accel_columns_ordered) | |
97 | + | |
98 | + gyro_df = pd.DataFrame({ | |
99 | + 'trip_id': data["trip_id"], | |
100 | + 'user_id': data["user_id"], | |
101 | + 'gyro_x': data["gyro"]["x"], | |
102 | + 'gyro_y': data["gyro"]["y"], | |
103 | + 'gyro_z': data["gyro"]["z"], | |
104 | + 'timestamp': data["gyro"]["timestamp"] | |
105 | + }) | |
106 | + gyro_columns_ordered = ['trip_id', 'user_id', 'timestamp', 'gyro_x', 'gyro_y', 'gyro_z'] | |
107 | + gyro_csv_data = gyro_df[gyro_columns_ordered].to_csv(header=False, index=False) | |
108 | + db.insert_gyro_data(gyro_csv_data, gyro_columns_ordered) | |
109 | + | |
110 | + motion_df = pd.DataFrame({ | |
111 | + 'trip_id': data["trip_id"], | |
112 | + 'user_id': data["user_id"], | |
113 | + 'motion_pitch': data["motion"]["pitch"], | |
114 | + 'motion_roll': data["motion"]["roll"], | |
115 | + 'motion_yaw': data["motion"]["yaw"], | |
116 | + 'timestamp': data["motion"]["timestamp"] | |
117 | + }) | |
118 | + motion_columns_ordered = ['trip_id', 'user_id', 'timestamp', 'motion_pitch', 'motion_roll', 'motion_yaw'] | |
119 | + motion_csv_data = motion_df[motion_columns_ordered].to_csv(header=False, index=False) | |
120 | + db.insert_motion_data(motion_csv_data, motion_columns_ordered) | |
121 | + | |
122 | + azimuth_df = pd.DataFrame({ | |
123 | + 'trip_id': data["trip_id"], | |
124 | + 'user_id': data["user_id"], | |
125 | + 'timestamp': data["azimuth"]["timestamp"], | |
126 | + 'azimuth_heading': data["azimuth"]["heading"] | |
127 | + }) | |
128 | + azimuth_columns_ordered = ['trip_id', 'user_id', 'timestamp', 'azimuth_heading'] | |
129 | + azimuth_csv_data = azimuth_df[azimuth_columns_ordered].to_csv(header=False, index=False) | |
130 | + db.insert_azimuth_data(azimuth_csv_data, azimuth_columns_ordered) | |
131 | + | |
132 | + return {'result': f'success'} | |
133 | + | |
134 | + | |
135 | +@Action.route('/trip_and_score_update') | |
136 | +class TRIP_insert(Resource): | |
137 | + @Action.expect(trip_log_model) | |
138 | + @Action.doc(responses={200: 'Success'}) | |
139 | + @Action.doc(responses={401: 'Unauthorized'}) | |
140 | + @Action.doc(responses={500: 'Internal Error'}) | |
141 | + def post(self): | |
142 | + token = request.headers.get('Authorization') | |
143 | + | |
144 | + # Check if token is provided | |
145 | + if not token: | |
146 | + return {'result': 'fail', 'msg': '토큰이 없습니다.'}, 401 | |
147 | + | |
148 | + try: | |
149 | + # Decode the token to verify it | |
150 | + decoded_token = jwt.decode(token, "secret", algorithms=['HS256']) | |
151 | + user_id = decoded_token['id'] | |
152 | + except jwt.ExpiredSignatureError: | |
153 | + return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401 | |
154 | + except jwt.InvalidTokenError: | |
155 | + return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401 | |
156 | + | |
157 | + db = DB() | |
158 | + data = request.get_json() | |
159 | + if len(data["trip_id"]) != 64: | |
160 | + return {"result" : "ERROR! INVALID TRIP_ID!"}, 500 | |
161 | + | |
162 | + trip_id = data["trip_id"] | |
163 | + trip_distance_m = data["total_distance_m"] | |
164 | + trip_time_s = data["total_time_s"] | |
165 | + abrupt_start_count = data["abrupt_start_count"] | |
166 | + abrupt_stop_count = data["abrupt_stop_count"] | |
167 | + abrupt_acceleration_count = data["abrupt_acceleration_count"] | |
168 | + abrupt_deceleration_count = data["abrupt_deceleration_count"] | |
169 | + helmet_on = helmet_on = 1 if data["helmet_on"] == "true" else 0 | |
170 | + final_score = data["final_score"] | |
171 | + | |
172 | + if (helmet_on != 1) and (helmet_on != 0): | |
173 | + return {"result" : f"ERROR! INVALID 'helmet_on'! \n helmet_on : {helmet_on}"}, 500 | |
174 | + db.insert_trip_data( | |
175 | + user_id, | |
176 | + trip_id, | |
177 | + trip_distance_m, | |
178 | + trip_time_s, | |
179 | + abrupt_start_count, | |
180 | + abrupt_stop_count, | |
181 | + abrupt_acceleration_count, | |
182 | + abrupt_deceleration_count, | |
183 | + helmet_on, | |
184 | + final_score | |
185 | + ) | |
186 | + return {'result': f'success'} | |
187 | + | |
188 | +@Action.route('/get_history') | |
189 | +class Get_history(Resource): | |
190 | + @Action.expect(history_request_model) | |
191 | + @Action.doc(responses={401: 'Unauthorized'}) | |
192 | + @Action.doc(responses={500: 'Internal Error'}) | |
193 | + def post(self): | |
194 | + token = request.headers.get('Authorization') | |
195 | + | |
196 | + # Check if token is provided | |
197 | + if not token: | |
198 | + return {'result': 'fail', 'msg': '토큰이 없습니다.'}, 401 | |
199 | + | |
200 | + try: | |
201 | + # Decode the token to verify it | |
202 | + decoded_token = jwt.decode(token, "secret", algorithms=['HS256']) | |
203 | + user_id = decoded_token['id'] | |
204 | + except jwt.ExpiredSignatureError: | |
205 | + return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401 | |
206 | + except jwt.InvalidTokenError: | |
207 | + return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401 | |
208 | + | |
209 | + # Interact with the DB to get user history | |
210 | + | |
211 | + data = request.get_json() | |
212 | + user_id = data["user_id"] | |
213 | + try: | |
214 | + db = DB() | |
215 | + result, status_code = db.get_history(user_name=user_id) | |
216 | + return {'result': 'success', 'data': result}, status_code | |
217 | + except Exception as e: | |
218 | + print(str(e)) | |
219 | + return {'result': 'fail', 'msg': str(e)}, 500 | |
220 | + | |
221 | + | |
222 | +@Action.route('/get_history_main') | |
223 | +class Get_history_main(Resource): | |
224 | + @Action.expect(history_request_model) | |
225 | + @Action.doc(responses={401: 'Unauthorized'}) | |
226 | + @Action.doc(responses={500: 'Internal Error'}) | |
227 | + def post(self): | |
228 | + token = request.headers.get('Authorization') | |
229 | + if not token: | |
230 | + return {'result': 'fail', 'msg': '토큰이 없습니다.'}, 401 | |
231 | + | |
232 | + try: | |
233 | + decoded_token = jwt.decode(token, "secret", algorithms=['HS256']) | |
234 | + user_id = decoded_token['id'] | |
235 | + except jwt.ExpiredSignatureError: | |
236 | + return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401 | |
237 | + except jwt.InvalidTokenError: | |
238 | + return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401 | |
239 | + | |
240 | + data = request.get_json() | |
241 | + user_id = data["user_id"] | |
242 | + | |
243 | + try: | |
244 | + db = DB() | |
245 | + result, status_code = db.get_history_main(user_name=user_id) | |
246 | + | |
247 | + if not result: # `result`가 비어있는 경우 | |
248 | + return {'result': 'success', 'data': []}, 200 | |
249 | + return {'result': 'success', 'data': result}, status_code | |
250 | + except Exception as e: | |
251 | + print(str(e)) | |
252 | + return {'result': 'fail', 'msg': str(e)}, 500 | |
253 | + | |
254 | + | |
255 | +@Action.route('/get_history_by_period') | |
256 | +class GetHistoryByPeriod(Resource): | |
257 | + @Action.expect(history_request_model) # 요청 모델 정의 필요 | |
258 | + @Action.doc(responses={401: 'Unauthorized'}) | |
259 | + @Action.doc(responses={500: 'Internal Error'}) | |
260 | + def post(self): | |
261 | + token = request.headers.get('Authorization') | |
262 | + | |
263 | + if not token: | |
264 | + return {'result': 'fail', 'msg': '토큰이 없습니다.'}, 401 | |
265 | + | |
266 | + try: | |
267 | + decoded_token = jwt.decode(token, "secret", algorithms=['HS256']) | |
268 | + user_id = decoded_token['id'] | |
269 | + except jwt.ExpiredSignatureError: | |
270 | + return {'result': 'fail', 'msg': '토큰이 만료되었습니다.'}, 401 | |
271 | + except jwt.InvalidTokenError: | |
272 | + return {'result': 'fail', 'msg': '유효하지 않은 토큰입니다.'}, 401 | |
273 | + | |
274 | + # 요청 데이터 가져오기 | |
275 | + data = request.get_json() | |
276 | + start_date = data.get("start_date") | |
277 | + end_date = data.get("end_date") | |
278 | + | |
279 | + if not start_date or not end_date: | |
280 | + return {'result': 'fail', 'msg': 'start_date와 end_date가 필요합니다.'}, 400 | |
281 | + | |
282 | + try: | |
283 | + db = DB() | |
284 | + result, status_code = db.get_history_by_period(user_name=user_id, start_date=start_date, end_date=end_date) | |
285 | + return {'result': 'success', 'data': result}, status_code | |
286 | + except Exception as e: | |
287 | + print(str(e)) | |
288 | + return {'result': 'fail', 'msg': str(e)}, 500 | |
289 | + | |
290 | + | |
291 | +@Action.route('/ping') | |
292 | +class Ping(Resource): | |
293 | + def post(self): | |
294 | + return {"result" : 'success', "msg" : "pong!"}, 200 | |
295 | + | |
296 | + |
+++ auth.py
... | ... | @@ -0,0 +1,178 @@ |
1 | +from flask import request,jsonify | |
2 | +from flask_restx import Resource, Namespace, fields | |
3 | +from database.database import DB | |
4 | +import datetime | |
5 | +import jwt | |
6 | + | |
7 | + | |
8 | +users = {} | |
9 | + | |
10 | +Auth = Namespace( | |
11 | + name="Auth", | |
12 | + description="사용자 인증을 위한 API", | |
13 | +) | |
14 | + | |
15 | + | |
16 | +user_fields = Auth.model('User', { # Model 객체 생성 | |
17 | + 'id': fields.String(description='a User Name', required=True, example="id") | |
18 | +}) | |
19 | + | |
20 | + | |
21 | +user_fields_auth = Auth.inherit('User Auth', user_fields, { | |
22 | + 'password': fields.String(description='Password', required=True) | |
23 | + | |
24 | +}) | |
25 | + | |
26 | + | |
27 | +get_phone_number = Auth.inherit('get a phone number of an user', { | |
28 | + 'id' : fields.String(description="user id", required=True) | |
29 | +}) | |
30 | + | |
31 | + | |
32 | +get_email = Auth.inherit('get an email of an user', { | |
33 | + 'id' : fields.String(description="user id", required=True) | |
34 | +}) | |
35 | + | |
36 | +user_fields_register = Auth.inherit('User reigster', user_fields, { | |
37 | + 'password': fields.String(description='Password', required=True),'email': fields.String(description='email', required=True),'phone': fields.String(description='phone', required=True) | |
38 | + | |
39 | +}) | |
40 | + | |
41 | + | |
42 | + | |
43 | +@Auth.route('/id') | |
44 | +class AuthCheck(Resource): | |
45 | + @Auth.doc(responses={200: 'Success'}) | |
46 | + @Auth.doc(responses={500: 'Register Failed'}) | |
47 | + def post(self): | |
48 | + db=DB() | |
49 | + id = request.json['id'] | |
50 | + value=db.db_check_id(id) | |
51 | + if value != None: | |
52 | + return { | |
53 | + "message": "중복 아이디가 있습니다" | |
54 | + }, 500 | |
55 | + else: | |
56 | + return { | |
57 | + 'message': '사용가능한 아이디입니다' # str으로 반환하여 return | |
58 | + }, 200 | |
59 | + | |
60 | + | |
61 | +@Auth.route('/register') | |
62 | +class AuthRegister(Resource): | |
63 | + @Auth.expect(user_fields_register) | |
64 | + @Auth.doc(responses={200: 'Success'}) | |
65 | + @Auth.doc(responses={500: 'Register Failed'}) | |
66 | + def post(self): | |
67 | + user_manager = DB() | |
68 | + # Extract data from the request | |
69 | + data = request.json | |
70 | + id_ = data['id'] | |
71 | + password = data['password'] | |
72 | + user_email = data['email'] | |
73 | + # sex = data['user_sex'] | |
74 | + phone = data['phone'] | |
75 | + | |
76 | + # Prepare data for registration | |
77 | + user_data = { | |
78 | + 'username': id_, | |
79 | + 'password': password, | |
80 | + 'email': user_email, | |
81 | + # 'sex': sex, | |
82 | + 'phone': phone | |
83 | + } | |
84 | + | |
85 | + # Call the register_user method from the UserManager instance | |
86 | + result, status_code = user_manager.register_user(user_data) | |
87 | + | |
88 | + # Return the appropriate response based on the result from UserManager | |
89 | + if status_code == 200: | |
90 | + return result, 200 | |
91 | + else: | |
92 | + return result, 500 | |
93 | + | |
94 | +@Auth.route('/retrive_phone_number') | |
95 | +class AuthRegister(Resource): | |
96 | + @Auth.expect(get_phone_number) | |
97 | + @Auth.doc(responses={200: 'Success'}) | |
98 | + @Auth.doc(responses={500: 'Register Failed'}) | |
99 | + def post(self): | |
100 | + user_manager = DB() | |
101 | + data = request.json | |
102 | + id_ = data['id'] | |
103 | + query_input = { | |
104 | + "username" : id_ | |
105 | + } | |
106 | + result, status_code = user_manager.get_phone_number(query_input) | |
107 | + | |
108 | + if status_code == 200: | |
109 | + return result, 200 | |
110 | + else: | |
111 | + return result, 500 | |
112 | + | |
113 | + | |
114 | +@Auth.route('/retrive_email') | |
115 | +class AuthRegister(Resource): | |
116 | + @Auth.expect(get_email) | |
117 | + @Auth.doc(responses={200: 'Success'}) | |
118 | + @Auth.doc(responses={500: 'Register Failed'}) | |
119 | + def post(self): | |
120 | + user_manager = DB() | |
121 | + data = request.json | |
122 | + id_ = data['id'] | |
123 | + query_input = { | |
124 | + "username" : id_ | |
125 | + } | |
126 | + result, status_code = user_manager.get_email(query_input) | |
127 | + | |
128 | + if status_code == 200: | |
129 | + return result, 200 | |
130 | + else: | |
131 | + return result, 500 | |
132 | + | |
133 | + | |
134 | + | |
135 | +@Auth.route('/login') | |
136 | +class AuthLogin(Resource): | |
137 | + @Auth.expect(user_fields_auth) | |
138 | + @Auth.doc(responses={200: 'Login Successful'}) | |
139 | + @Auth.doc(responses={401: 'Unauthorized'}) | |
140 | + @Auth.doc(responses={500: 'Login Failed'}) | |
141 | + def post(self): | |
142 | + user_manager = DB() | |
143 | + # Extract data from the request | |
144 | + data = request.json | |
145 | + id_ = data['id'] | |
146 | + password = data['password'] | |
147 | + | |
148 | + # Prepare data for authentication | |
149 | + user_data = { | |
150 | + 'username': id_, | |
151 | + 'password': password | |
152 | + } | |
153 | + | |
154 | + # Call the login_user method from the UserManager instance | |
155 | + result, status_code = user_manager.login_user(user_data) | |
156 | + | |
157 | + if result['status'] == 'success': | |
158 | + payload = { | |
159 | + 'id': id_, | |
160 | + 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=14) | |
161 | + } | |
162 | + token = jwt.encode(payload, "secret", algorithm='HS256') | |
163 | + return {'result': 'success', 'token': token}, 200 | |
164 | + else : | |
165 | + return {'result': 'fail', 'msg': '아이디/비밀번호가 일치하지 않습니다.'}, 401 | |
166 | + | |
167 | + | |
168 | +@Auth.route('/withdraw') | |
169 | +class AuthWithdraw(Resource): | |
170 | + def post(self): | |
171 | + db=DB() | |
172 | + id = request.json['token'] | |
173 | + payload = jwt.decode(id, "secret", algorithms=['HS256']) | |
174 | + db.db_delete_id(payload['id']) | |
175 | + return {'secession':'success'}, 200 | |
176 | + | |
177 | + | |
178 | + |
+++ database/database.py
... | ... | @@ -0,0 +1,544 @@ |
1 | +import psycopg2 # driver 임포트 | |
2 | +import json | |
3 | +import bcrypt | |
4 | +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | |
5 | +from cryptography.hazmat.primitives import padding | |
6 | +from cryptography.hazmat.backends import default_backend | |
7 | +import re | |
8 | +import os | |
9 | +from io import StringIO | |
10 | +from datetime import datetime, timedelta | |
11 | + | |
12 | +config_file_path = "database/db_config.json" | |
13 | + | |
14 | +class DB(): | |
15 | + def __init__(self): | |
16 | + | |
17 | + # Load the database configuration from the JSON file | |
18 | + self.db_config = self.load_db_config(config_file_path) | |
19 | + | |
20 | + # Initialize database connection | |
21 | + self.conn = psycopg2.connect( | |
22 | + host=self.db_config['host'], | |
23 | + dbname=self.db_config['dbname'], | |
24 | + user=self.db_config['user'], | |
25 | + password=self.db_config['password'], | |
26 | + port=self.db_config['port'], | |
27 | + # options=self.db_config['options'] | |
28 | + ) | |
29 | + self.schema = self.db_config["schema"] | |
30 | + | |
31 | + self.conn.autocommit=True | |
32 | + self.cur = self.conn.cursor() | |
33 | + # yeah, that double quotation is absolutely needed (to distinguish capital letters) | |
34 | + self.cur.execute("SET search_path TO " + f'"{self.schema}"') | |
35 | + with open("database/keys/encryption_key2025-04-03_14:33:09", "rb") as f: | |
36 | + self.encryption_key = f.read() | |
37 | + | |
38 | + def load_db_config(self, config_file_path): | |
39 | + """ | |
40 | + Loads database configuration from a JSON file. | |
41 | + """ | |
42 | + with open(config_file_path, 'r') as config_file: | |
43 | + return json.load(config_file) | |
44 | + | |
45 | + def encrypt_aes(self, plain_text): | |
46 | + iv = os.urandom(16) # AES block size is 16 bytes | |
47 | + cipher = Cipher(algorithms.AES(self.encryption_key), modes.CBC(iv), backend=default_backend()) | |
48 | + encryptor = cipher.encryptor() | |
49 | + | |
50 | + # Pad the plaintext to be a multiple of 16 bytes | |
51 | + padder = padding.PKCS7(algorithms.AES.block_size).padder() | |
52 | + padded_data = padder.update(plain_text.encode('utf-8')) + padder.finalize() | |
53 | + | |
54 | + encrypted_data = encryptor.update(padded_data) + encryptor.finalize() | |
55 | + return encrypted_data, iv | |
56 | + | |
57 | + def decrypt_aes(self, encrypted_data, iv): | |
58 | + cipher = Cipher(algorithms.AES(self.encryption_key), modes.CBC(iv), backend=default_backend()) | |
59 | + decryptor = cipher.decryptor() | |
60 | + | |
61 | + decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize() | |
62 | + | |
63 | + # Remove padding after decryption | |
64 | + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() | |
65 | + unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize() | |
66 | + | |
67 | + return unpadded_data.decode('utf-8') | |
68 | + | |
69 | + def cleanse_and_validate_input(self, data): | |
70 | + """ | |
71 | + Cleanses input by removing leading/trailing spaces and validates the data. | |
72 | + Returns cleansed data and an error message if validation fails. | |
73 | + """ | |
74 | + username = data.get('username', '').strip() | |
75 | + password = data.get('password', '').strip() | |
76 | + email = data.get('email', '').strip() | |
77 | + phone = data.get('phone', '').strip() | |
78 | + phone = phone.replace("-","") | |
79 | + sex = data.get('sex', '').strip() | |
80 | + | |
81 | + # Validate username | |
82 | + if not username: | |
83 | + return None, "Username is required." | |
84 | + if len(username) > 26: | |
85 | + return None, "Username must not exceed 26 characters." | |
86 | + | |
87 | + # Validate password | |
88 | + if not password: | |
89 | + return None, "Password is required." | |
90 | + if len(password) < 8: | |
91 | + return None, "Password must be at least 8 characters long." | |
92 | + | |
93 | + # Validate email format | |
94 | + if not email or not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email): | |
95 | + return None, "Invalid email address." | |
96 | + | |
97 | + # Validate phone number format | |
98 | + if not re.fullmatch(r'010\d{8}', phone): | |
99 | + return None, "Phone number must be in the format 010XXXXXXXX where X are digits." | |
100 | + | |
101 | + # Validate sex input | |
102 | + # if not sex: | |
103 | + # return None, "Sex is required." | |
104 | + # if sex not in ['Male', 'Female', 'Non-binary', 'Other']: | |
105 | + # return None, "Invalid value for sex." | |
106 | + sex = "WHATEVER" | |
107 | + return { | |
108 | + 'username': username, | |
109 | + 'password': password, | |
110 | + 'email': email, | |
111 | + 'phone': phone, | |
112 | + 'sex': sex | |
113 | + }, None | |
114 | + | |
115 | + def register_user(self, data): | |
116 | + data, error = self.cleanse_and_validate_input(data) | |
117 | + if error: | |
118 | + return {'status': 'error', 'message': error}, 400 | |
119 | + | |
120 | + username = data['username'] | |
121 | + password = data['password'] | |
122 | + email = data['email'] | |
123 | + phone = data['phone'] | |
124 | + sex = data['sex'] | |
125 | + | |
126 | + # Hash the password with bcrypt, which automatically handles the salt | |
127 | + hashed_pw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) | |
128 | + | |
129 | + # Encrypt email, phone, and sex with AES | |
130 | + encrypted_email, email_iv = self.encrypt_aes(email) | |
131 | + encrypted_phone, phone_iv = self.encrypt_aes(phone) | |
132 | + encrypted_sex, sex_iv = self.encrypt_aes(sex) | |
133 | + | |
134 | + # Insert the user into the database | |
135 | + try: | |
136 | + self.cur.execute(f""" | |
137 | + INSERT INTO users (username, user_pw, user_email, email_iv, user_phone, phone_iv, user_time_stamp) | |
138 | + VALUES (%s, %s, %s, %s, %s, %s, %s) | |
139 | + """, ( | |
140 | + username, | |
141 | + psycopg2.Binary(hashed_pw), | |
142 | + psycopg2.Binary(encrypted_email), | |
143 | + psycopg2.Binary(email_iv), | |
144 | + psycopg2.Binary(encrypted_phone), | |
145 | + psycopg2.Binary(phone_iv), | |
146 | + datetime.now() # Correct way to insert current timestamp with timezone | |
147 | + ) | |
148 | + ) | |
149 | + self.conn.commit() | |
150 | + return {'status': 'success', 'message': f'user {username} registered successfully'}, 200 | |
151 | + except psycopg2.Error as e: | |
152 | + self.conn.rollback() | |
153 | + return {'status': 'error', 'message': str(e)}, 400 | |
154 | + | |
155 | + def login_user(self, data): | |
156 | + username = data.get('username', '').strip() | |
157 | + password = data.get('password', '').strip() | |
158 | + | |
159 | + # Validate input | |
160 | + if not username or not password: | |
161 | + return {'status': 'error', 'message': 'Username and password are required.'}, 400 | |
162 | + | |
163 | + # Retrieve the user's hashed password from the database | |
164 | + self.cur.execute("SELECT user_pw FROM users WHERE username = %s", (username,)) | |
165 | + user = self.cur.fetchone() | |
166 | + | |
167 | + if user is None: | |
168 | + return {'status': 'error', 'message': 'Invalid username or password'}, 401 | |
169 | + | |
170 | + hashed_pw = bytes(user[0]) # Convert the retrieved hashed password to bytes | |
171 | + | |
172 | + # Check if the provided password matches the stored hashed password | |
173 | + if bcrypt.checkpw(password.encode('utf-8'), hashed_pw): | |
174 | + return {'status': 'success', 'message': 'Logged in successfully'}, 200 | |
175 | + else: | |
176 | + return {'status': 'error', 'message': 'Invalid username or password'}, 401 | |
177 | + | |
178 | + def get_phone_number(self, data): | |
179 | + username = data.get('username', '').strip() | |
180 | + | |
181 | + if not username: | |
182 | + return {'status': 'error', 'message': 'Username is required.'}, 400 | |
183 | + | |
184 | + # Retrieve the encrypted phone number and IV from the database | |
185 | + self.cur.execute("SELECT user_phone, phone_iv FROM users WHERE username = %s", (username,)) | |
186 | + user = self.cur.fetchone() | |
187 | + | |
188 | + if user is None: | |
189 | + return {'status': 'error', 'message': 'User not found'}, 404 | |
190 | + | |
191 | + encrypted_phone, phone_iv = user | |
192 | + | |
193 | + # Decrypt the phone number | |
194 | + decrypted_phone = self.decrypt_aes(encrypted_phone, phone_iv) | |
195 | + | |
196 | + return {'status': 'success', 'phone_number': decrypted_phone}, 200 | |
197 | + | |
198 | + | |
199 | + def get_email(self, data): | |
200 | + username = data.get('username', '').strip() | |
201 | + | |
202 | + if not username: | |
203 | + return {'status': 'error', 'message': 'Username is required.'}, 400 | |
204 | + | |
205 | + # Retrieve the encrypted phone number and IV from the database | |
206 | + self.cur.execute("SELECT user_email, email_iv FROM users WHERE username = %s", (username,)) | |
207 | + user = self.cur.fetchone() | |
208 | + | |
209 | + if user is None: | |
210 | + return {'status': 'error', 'message': 'User not found'}, 404 | |
211 | + | |
212 | + encrypted_phone, phone_iv = user | |
213 | + | |
214 | + # Decrypt the phone number | |
215 | + decrypted_phone = self.decrypt_aes(encrypted_phone, phone_iv) | |
216 | + | |
217 | + return {'status': 'success', 'phone_number': decrypted_phone}, 200 | |
218 | + | |
219 | + def insert_gps_data(self, csv_block, columns): | |
220 | + cur = self.conn.cursor() | |
221 | + data = StringIO(csv_block) | |
222 | + | |
223 | + # using COPY instead of INSERT to do even less operation per data. | |
224 | + cur.copy_from(data, 'gps_data', sep=',', columns=columns) | |
225 | + self.conn.commit() | |
226 | + cur.close() | |
227 | + return True | |
228 | + | |
229 | + def insert_accel_data(self, csv_block, columns): | |
230 | + cur = self.conn.cursor() | |
231 | + try: | |
232 | + data = StringIO(csv_block) # Create StringIO internally | |
233 | + cur.copy_from(data, f'accel_data', sep=',', columns=columns) | |
234 | + self.conn.commit() | |
235 | + success = True | |
236 | + except Exception as e: | |
237 | + self.conn.rollback() | |
238 | + print(f"DB Error inserting Accel: {e}") | |
239 | + success = False | |
240 | + finally: | |
241 | + if cur: | |
242 | + cur.close() | |
243 | + return success | |
244 | + | |
245 | + def insert_gyro_data(self, csv_block, columns): | |
246 | + cur = self.conn.cursor() | |
247 | + try: | |
248 | + data = StringIO(csv_block) # Create StringIO internally | |
249 | + cur.copy_from(data, f'gyro_data', sep=',', columns=columns) | |
250 | + self.conn.commit() | |
251 | + success = True | |
252 | + except Exception as e: | |
253 | + self.conn.rollback() | |
254 | + print(f"DB Error inserting Gyro: {e}") | |
255 | + success = False | |
256 | + finally: | |
257 | + if cur: | |
258 | + cur.close() | |
259 | + return success | |
260 | + | |
261 | + def insert_motion_data(self, csv_block, columns): | |
262 | + cur = self.conn.cursor() | |
263 | + try: | |
264 | + data = StringIO(csv_block) # Create StringIO internally | |
265 | + cur.copy_from(data, f'motion_data', sep=',', columns=columns) | |
266 | + self.conn.commit() | |
267 | + success = True | |
268 | + except Exception as e: | |
269 | + self.conn.rollback() | |
270 | + print(f"DB Error inserting Motion: {e}") | |
271 | + success = False | |
272 | + finally: | |
273 | + if cur: | |
274 | + cur.close() | |
275 | + return success | |
276 | + | |
277 | + def insert_azimuth_data(self, csv_block, columns): | |
278 | + cur = self.conn.cursor() | |
279 | + try: | |
280 | + data = StringIO(csv_block) # Create StringIO internally | |
281 | + cur.copy_from(data, f'azimuth_data', sep=',', columns=columns) | |
282 | + self.conn.commit() | |
283 | + success = True | |
284 | + except Exception as e: | |
285 | + self.conn.rollback() | |
286 | + print(f"DB Error inserting Azimuth: {e}") | |
287 | + success = False | |
288 | + finally: | |
289 | + if cur: | |
290 | + cur.close() | |
291 | + return success | |
292 | + | |
293 | + def insert_trip_data( | |
294 | + self, | |
295 | + username, | |
296 | + trip_id, | |
297 | + total_distance_m, | |
298 | + total_time_s, | |
299 | + abrupt_start_count, | |
300 | + abrupt_stop_count, | |
301 | + abrupt_acceleration_count, | |
302 | + abrupt_deceleration_count, | |
303 | + helmet_on, | |
304 | + final_score | |
305 | + ): | |
306 | + | |
307 | + self.cur.execute(f""" | |
308 | + INSERT INTO trip_log (user_id, trip_id, timestamp, total_distance_m, total_time_s, abrupt_start_count, abrupt_stop_count, | |
309 | + abrupt_acceleration_count, abrupt_deceleration_count, helmet_on, final_score) | |
310 | + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) | |
311 | + """, ( | |
312 | + username, | |
313 | + trip_id, | |
314 | + datetime.now(), | |
315 | + total_distance_m, | |
316 | + total_time_s, | |
317 | + abrupt_start_count, | |
318 | + abrupt_stop_count, | |
319 | + abrupt_acceleration_count, | |
320 | + abrupt_deceleration_count, | |
321 | + helmet_on, | |
322 | + final_score | |
323 | + ) | |
324 | + ) | |
325 | + | |
326 | + def db_delete_id(self,user_id) : | |
327 | + cur = self.conn.cursor() | |
328 | + cur.execute(f''' | |
329 | + delete | |
330 | + from "{self.schema}".user_id ui | |
331 | + where user_id = '{user_id}' | |
332 | + ''') | |
333 | + cur.close() | |
334 | + | |
335 | + def get_history(self, user_name): | |
336 | + """ | |
337 | + Retrieves all trip logs for the specified user within the last month and returns them in JSON format. | |
338 | + [ | |
339 | + { | |
340 | + "trip_id": "trip_001", | |
341 | + "timestamp": "2024-09-01 12:45:00", | |
342 | + "total_distance_m": 1000.5, | |
343 | + "total_time_s": 600, | |
344 | + "abrupt_start_count": 3, | |
345 | + "abrupt_stop_count": 2, | |
346 | + "abrupt_acceleration_count": 1, | |
347 | + "abrupt_deceleration_count": 1, | |
348 | + "helmet_on": true, | |
349 | + "final_score": 85.5 | |
350 | + }, | |
351 | + { | |
352 | + "trip_id": "trip_002", | |
353 | + "timestamp": "2024-09-02 14:30:00", | |
354 | + "total_distance_m": 1500.0, | |
355 | + "total_time_s": 720, | |
356 | + "abrupt_start_count": 2, | |
357 | + "abrupt_stop_count": 3, | |
358 | + "abrupt_acceleration_count": 1, | |
359 | + "abrupt_deceleration_count": 2, | |
360 | + "helmet_on": false, | |
361 | + "final_score": 90.0 | |
362 | + } | |
363 | + ] | |
364 | + """ | |
365 | + try: | |
366 | + # Execute the query to retrieve logs within the last month | |
367 | + self.cur.execute(""" | |
368 | + SELECT trip_id, timestamp, total_distance_m, total_time_s, abrupt_start_count, | |
369 | + abrupt_stop_count, abrupt_acceleration_count, abrupt_deceleration_count, | |
370 | + helmet_on, final_score | |
371 | + FROM trip_log | |
372 | + WHERE user_id = %s | |
373 | + AND timestamp >= NOW() - INTERVAL '1 month' | |
374 | + """, (user_name,)) | |
375 | + | |
376 | + # Fetch all results | |
377 | + rows = self.cur.fetchall() | |
378 | + | |
379 | + # Format the results into a list of dictionaries | |
380 | + result = [] | |
381 | + for row in rows: | |
382 | + trip_log = { | |
383 | + "trip_id": row[0], | |
384 | + "timestamp": row[1].strftime("%Y-%m-%d %H:%M:%S"), # Format timestamp | |
385 | + "total_distance_m": row[2], | |
386 | + "total_time_s": row[3], | |
387 | + "abrupt_start_count": row[4], | |
388 | + "abrupt_stop_count": row[5], | |
389 | + "abrupt_acceleration_count": row[6], | |
390 | + "abrupt_deceleration_count": row[7], | |
391 | + "helmet_on": bool(row[8]), # Convert INT to Boolean | |
392 | + "final_score": row[9] | |
393 | + } | |
394 | + result.append(trip_log) | |
395 | + | |
396 | + # Convert the result list to JSON format | |
397 | + return json.dumps(result), 200 | |
398 | + | |
399 | + except psycopg2.Error as e: | |
400 | + self.conn.rollback() | |
401 | + return {'status': 'error', 'message': str(e)}, 500 | |
402 | + | |
403 | + def get_history_main(self, user_name): | |
404 | + try: | |
405 | + # 최신 기록 1건 조회 | |
406 | + self.cur.execute(""" | |
407 | + SELECT trip_id, timestamp, total_distance_m, total_time_s, abrupt_start_count, | |
408 | + abrupt_stop_count, abrupt_acceleration_count, abrupt_deceleration_count, | |
409 | + helmet_on, final_score | |
410 | + FROM trip_log | |
411 | + WHERE user_id = %s | |
412 | + ORDER BY timestamp DESC | |
413 | + LIMIT 1 | |
414 | + """, (user_name,)) | |
415 | + row = self.cur.fetchone() | |
416 | + | |
417 | + if not row: | |
418 | + return json.dumps({"message": "No records found", "data": []}), 200 | |
419 | + | |
420 | + latest_record = { | |
421 | + "trip_id": row[0], | |
422 | + "timestamp": row[1].strftime("%Y-%m-%d %H:%M:%S"), | |
423 | + "total_distance_m": row[2], | |
424 | + "total_time_s": row[3], | |
425 | + "abrupt_start_count": row[4], | |
426 | + "abrupt_stop_count": row[5], | |
427 | + "abrupt_acceleration_count": row[6], | |
428 | + "abrupt_deceleration_count": row[7], | |
429 | + "helmet_on": bool(row[8]), | |
430 | + "final_score": row[9] | |
431 | + } | |
432 | + | |
433 | + # 전체 기록 수 조회 | |
434 | + self.cur.execute("SELECT COUNT(*) FROM trip_log WHERE user_id = %s", (user_name,)) | |
435 | + total_count = self.cur.fetchone()[0] | |
436 | + | |
437 | + # final_score가 95점 이상인 기록 수 조회 | |
438 | + self.cur.execute("SELECT COUNT(*) FROM trip_log WHERE user_id = %s AND final_score >= 95", (user_name,)) | |
439 | + count_above_95 = self.cur.fetchone()[0] | |
440 | + | |
441 | + # 평균 final_score 조회 | |
442 | + self.cur.execute("SELECT AVG(final_score) FROM trip_log WHERE user_id = %s", (user_name,)) | |
443 | + avg_final_score = self.cur.fetchone()[0] | |
444 | + avg_final_score = round(avg_final_score, 2) | |
445 | + | |
446 | + # 응답 데이터 구성 | |
447 | + response_data = { | |
448 | + "latest_record": latest_record, | |
449 | + "total_count": total_count, | |
450 | + "count_above_95": count_above_95, | |
451 | + "average_final_score": avg_final_score | |
452 | + } | |
453 | + | |
454 | + return json.dumps(response_data), 200 | |
455 | + | |
456 | + except psycopg2.Error as e: | |
457 | + self.conn.rollback() | |
458 | + return {'status': 'error', 'message': str(e)}, 500 | |
459 | + | |
460 | + def get_history_by_period(self, user_name, start_date, end_date): | |
461 | + try: | |
462 | + # Execute the query to retrieve logs within the specified period | |
463 | + self.cur.execute(""" | |
464 | + SELECT trip_id, timestamp, total_distance_m, total_time_s, abrupt_start_count, | |
465 | + abrupt_stop_count, abrupt_acceleration_count, abrupt_deceleration_count, | |
466 | + helmet_on, final_score | |
467 | + FROM trip_log | |
468 | + WHERE user_id = %s | |
469 | + AND timestamp BETWEEN %s AND %s | |
470 | + """, (user_name, start_date, end_date)) | |
471 | + | |
472 | + # Fetch all results | |
473 | + rows = self.cur.fetchall() | |
474 | + | |
475 | + # Format the results into a list of dictionaries | |
476 | + result = [] | |
477 | + final_scores = [] | |
478 | + daily_scores = {} | |
479 | + | |
480 | + for row in rows: | |
481 | + trip_log = { | |
482 | + "trip_id": row[0], | |
483 | + "timestamp": row[1].strftime("%Y-%m-%d %H:%M:%S"), # Format timestamp | |
484 | + "total_distance_m": row[2], | |
485 | + "total_time_s": row[3], | |
486 | + "abrupt_start_count": row[4], | |
487 | + "abrupt_stop_count": row[5], | |
488 | + "abrupt_acceleration_count": row[6], | |
489 | + "abrupt_deceleration_count": row[7], | |
490 | + "helmet_on": bool(row[8]), # Convert INT to Boolean | |
491 | + "final_score": row[9] | |
492 | + } | |
493 | + result.append(trip_log) | |
494 | + final_scores.append(row[9]) | |
495 | + | |
496 | + # 날짜별 평균 계산을 위한 데이터 정리 | |
497 | + date_key = row[1].strftime("%Y-%m-%d") | |
498 | + if date_key not in daily_scores: | |
499 | + daily_scores[date_key] = [] | |
500 | + daily_scores[date_key].append(row[9]) | |
501 | + | |
502 | + # 전체 기간의 final_score 평균 계산 | |
503 | + overall_avg_score = round(sum(final_scores) / len(final_scores), 2) if final_scores else 0 | |
504 | + | |
505 | + # start_date와 end_date를 datetime 객체로 변환 (이미 datetime이라면 그대로 사용) | |
506 | + if isinstance(start_date, str): | |
507 | + start_dt = datetime.strptime(start_date, "%Y-%m-%d") | |
508 | + else: | |
509 | + start_dt = start_date | |
510 | + | |
511 | + if isinstance(end_date, str): | |
512 | + end_dt = datetime.strptime(end_date, "%Y-%m-%d") | |
513 | + else: | |
514 | + end_dt = end_date | |
515 | + | |
516 | + # start_date부터 end_date까지 모든 날짜에 대해 평균 점수를 계산 (데이터가 없으면 0) | |
517 | + daily_avg_scores = {} | |
518 | + current_dt = start_dt | |
519 | + while current_dt <= end_dt: | |
520 | + day_key = current_dt.day # 예: 1, 2, 3 등 | |
521 | + full_date_key = current_dt.strftime("%Y-%m-%d") | |
522 | + scores = daily_scores.get(full_date_key, []) | |
523 | + daily_avg_scores[day_key] = round(sum(scores) / len(scores), 2) if scores else 0 | |
524 | + current_dt += timedelta(days=1) | |
525 | + | |
526 | + # Convert the result list to JSON format | |
527 | + response_data = { | |
528 | + "history": result, | |
529 | + "overall_avg_score": overall_avg_score, | |
530 | + "daily_avg_scores": daily_avg_scores | |
531 | + } | |
532 | + | |
533 | + return json.dumps(response_data), 200 | |
534 | + | |
535 | + except psycopg2.Error as e: | |
536 | + self.conn.rollback() | |
537 | + return {'status': 'error', 'message': str(e)}, 500 | |
538 | + | |
539 | + def close_connection(self): | |
540 | + cur = self.cur | |
541 | + cur.close() | |
542 | + return True | |
543 | + | |
544 | + |
+++ database/key_gen.py
... | ... | @@ -0,0 +1,18 @@ |
1 | +import os | |
2 | + | |
3 | +# NEVER be the part of server script, THIS SHOULD NEVER run with server. | |
4 | +# ALSO, remember to BACKUP the key | |
5 | + | |
6 | +def create_and_save_key(key_file_path): | |
7 | + """ | |
8 | + Generates a new AES encryption key and saves it to a file. | |
9 | + """ | |
10 | + key = os.urandom(32) # AES-256 requires a 32-byte key | |
11 | + with open(key_file_path, 'wb') as key_file: | |
12 | + key_file.write(key) | |
13 | + print(f"Encryption key created and saved to {key_file_path}") | |
14 | + return key | |
15 | + | |
16 | +if __name__ == "__main__": | |
17 | + from datetime import datetime | |
18 | + create_and_save_key(f"keys/encryption_key{datetime.now().strftime('%Y-%m-%d_%H:%M:%S')}")(파일 끝에 줄바꿈 문자 없음) |
+++ logger/__pycache__/logger.cpython-310.pyc
Binary file is not shown |
+++ logger/__pycache__/logger.cpython-311.pyc
Binary file is not shown |
+++ logger/logger.py
... | ... | @@ -0,0 +1,14 @@ |
1 | +from datetime import datetime | |
2 | +import os | |
3 | + | |
4 | +if not os.path.exists("logs"): | |
5 | + os.makedirs("logs") | |
6 | + os.makedirs("logs/gunicorn") | |
7 | + | |
8 | +bind = "192.168.0.195:27461" | |
9 | +workers = 2 | |
10 | +#worker_type = 'gevent' | |
11 | +reload = True | |
12 | +accesslog = f"./logs/gunicorn/access_{datetime.now().strftime('%Y-%m-%d_%H')}.log" | |
13 | +errorlog = f"./logs/gunicorn/error_{datetime.now().strftime('%Y-%m-%d_%H')}.log" | |
14 | +loglevel = "info" |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?